From e3ff9547a6d54a04d3620128bcbfdd61033c0b12 Mon Sep 17 00:00:00 2001 From: Roman Dobosz Date: Wed, 23 Dec 2020 10:32:39 +0100 Subject: [PATCH] Added function for figure out link for the resource. Because Kubernetes will stop propagating metadata.selLink field in release 1.20 and it will be removed in release 1.21, we need to adapt and calculate selfLink equivalent by ourselves. In this patch new function is introduced, which will return the path for provided resource. Also, we need to deal with list of resources, since for some reason CRs do have information regarding apiVersion and the kind, while as for core resources the apiVersion is only on top of the list object, kind is something like *List, and object within 'items' are without either apiVersion nor kind. Implements: blueprint selflink Change-Id: I721a46ea0379382f7eb2e13c59bd193314f37e7f --- kuryr_kubernetes/k8s_client.py | 29 ++++++++- .../tests/unit/test_k8s_client.py | 26 +++++++- kuryr_kubernetes/tests/unit/test_utils.py | 63 ++++++++++++++++++ kuryr_kubernetes/utils.py | 65 +++++++++++++++++++ kuryr_kubernetes/watcher.py | 12 +--- 5 files changed, 184 insertions(+), 11 deletions(-) diff --git a/kuryr_kubernetes/k8s_client.py b/kuryr_kubernetes/k8s_client.py index 535ef1f45..220cd8322 100644 --- a/kuryr_kubernetes/k8s_client.py +++ b/kuryr_kubernetes/k8s_client.py @@ -116,7 +116,34 @@ class K8sClient(object): url = self._base_url + path response = self.session.get(url, headers=headers) self._raise_from_response(response) - result = response.json() if json else response.text + + if json: + result = response.json() + kind = result['kind'] + + api_version = result.get('apiVersion') + if not api_version: + api_version = utils.get_api_ver(path) + + # Strip List from e.g. PodList. For some reason `.items` of a list + # returned from API doesn't have `kind` set. + # NOTE(gryf): Also, for the sake of calculating selfLink + # equivalent, we need to have both: kind and apiVersion, while the + # latter is not present on items list for core resources, while + # for custom resources there are both kind and apiVersion.. + if kind.endswith('List'): + kind = kind[:-4] + for item in result['items']: + if not item.get('kind'): + item['kind'] = kind + if not item.get('apiVersion'): + item['apiVersion'] = api_version + else: + if not result.get('apiVersion'): + result['apiVersion'] = api_version + else: + result = response.text + return result def _get_url_and_header(self, path, content_type): diff --git a/kuryr_kubernetes/tests/unit/test_k8s_client.py b/kuryr_kubernetes/tests/unit/test_k8s_client.py index 8f2d5ab7f..b73dbf829 100644 --- a/kuryr_kubernetes/tests/unit/test_k8s_client.py +++ b/kuryr_kubernetes/tests/unit/test_k8s_client.py @@ -111,7 +111,7 @@ class TestK8sClient(test_base.TestCase): @mock.patch('requests.sessions.Session.get') def test_get(self, m_get): path = '/test' - ret = {'test': 'value'} + ret = {'kind': 'Pod', 'apiVersion': 'v1'} m_resp = mock.MagicMock() m_resp.ok = True @@ -121,6 +121,30 @@ class TestK8sClient(test_base.TestCase): self.assertEqual(ret, self.client.get(path)) m_get.assert_called_once_with(self.base_url + path, headers=None) + @mock.patch('requests.sessions.Session.get') + def test_get_list(self, m_get): + path = '/test' + ret = {'kind': 'PodList', + 'apiVersion': 'v1', + 'items': [{'metadata': {'name': 'pod1'}, + 'spec': {}, + 'status': {}}]} + res = {'kind': 'PodList', + 'apiVersion': 'v1', + 'items': [{'metadata': {'name': 'pod1'}, + 'spec': {}, + 'status': {}, + 'kind': 'Pod', + 'apiVersion': 'v1'}]} + + m_resp = mock.MagicMock() + m_resp.ok = True + m_resp.json.return_value = ret + m_get.return_value = m_resp + + self.assertDictEqual(res, self.client.get(path)) + m_get.assert_called_once_with(self.base_url + path, headers=None) + @mock.patch('requests.sessions.Session.get') def test_get_exception(self, m_get): path = '/test' diff --git a/kuryr_kubernetes/tests/unit/test_utils.py b/kuryr_kubernetes/tests/unit/test_utils.py index 187dc93d9..4b0a0785a 100644 --- a/kuryr_kubernetes/tests/unit/test_utils.py +++ b/kuryr_kubernetes/tests/unit/test_utils.py @@ -375,3 +375,66 @@ class TestUtils(test_base.TestCase): self.assertEqual( target, ('10.0.1.208', 'test', 8080, '4472fab1-f01c-46a7-b197-5cba4f2d7135')) + + def test_get_res_link_core_res(self): + res = {'apiVersion': 'v1', + 'kind': 'Pod', + 'metadata': {'name': 'pod-1', + 'namespace': 'default'}} + self.assertEqual(utils.get_res_link(res), + '/api/v1/namespaces/default/pods/pod-1') + + def test_get_res_link_no_existent(self): + res = {'apiVersion': 'customapi/v1', + 'kind': 'ItsATrap!', + 'metadata': {'name': 'pod-1', + 'namespace': 'default'}} + self.assertRaises(KeyError, utils.get_res_link, res) + + def test_get_res_link_beta_res(self): + res = {'apiVersion': 'networking.k8s.io/v2beta2', + 'kind': 'NetworkPolicy', + 'metadata': {'name': 'np-1', + 'namespace': 'default'}} + self.assertEqual(utils.get_res_link(res), '/apis/networking.k8s.io/' + 'v2beta2/namespaces/default/networkpolicies/np-1') + + def test_get_res_link_no_namespace(self): + res = {'apiVersion': 'v1', + 'kind': 'Namespace', + 'metadata': {'name': 'ns-1'}} + + self.assertEqual(utils.get_res_link(res), '/api/v1/namespaces/ns-1') + + def test_get_res_link_custom_api(self): + res = {'apiVersion': 'openstack.org/v1', + 'kind': 'KuryrPort', + 'metadata': {'name': 'kp-1', + 'namespace': 'default'}} + + self.assertEqual(utils.get_res_link(res), + '/apis/openstack.org/v1/namespaces/default/' + 'kuryrports/kp-1') + + def test_get_res_link_no_apiversion(self): + res = {'kind': 'KuryrPort', + 'metadata': {'name': 'kp-1', + 'namespace': 'default'}} + self.assertRaises(KeyError, utils.get_res_link, res) + + def test_get_api_ver_core_api(self): + path = '/api/v1/namespaces/default/pods/pod-123' + self.assertEqual(utils.get_api_ver(path), 'v1') + + def test_get_api_ver_custom_resource(self): + path = '/apis/openstack.org/v1/namespaces/default/kuryrport/pod-123' + self.assertEqual(utils.get_api_ver(path), 'openstack.org/v1') + + def test_get_api_ver_random_path(self): + path = '/?search=foo' + self.assertRaises(ValueError, utils.get_api_ver, path) + + def test_get_res_selflink_still_available(self): + res = {'metadata': {'selfLink': '/foo'}} + + self.assertEqual(utils.get_res_link(res), '/foo') diff --git a/kuryr_kubernetes/utils.py b/kuryr_kubernetes/utils.py index 4d9d2c1bf..7f73483f9 100644 --- a/kuryr_kubernetes/utils.py +++ b/kuryr_kubernetes/utils.py @@ -12,6 +12,7 @@ import ipaddress import random +import re import socket import time @@ -72,6 +73,70 @@ MEMOIZE_NODE = cache.get_memoization_decorator( CONF, nodes_cache_region, "nodes_caching") cache.configure_cache_region(CONF, nodes_cache_region) +RESOURCE_MAP = {'Endpoints': 'endpoints', + 'KuryrLoadBalancer': 'kuryrloadbalancers', + 'KuryrPort': 'kuryrports', + 'KuryrNetworkPolicy': 'kuryrnetworkpolicies', + 'KuryrNetwork': 'kuryrnetworks', + 'Namespace': 'namespaces', + 'NetworkPolicy': 'networkpolicies', + 'Node': 'nodes', + 'Pod': 'pods', + 'Service': 'services'} +API_RE = re.compile(r'v\d+') + + +def get_res_link(obj): + """Return selfLink equivalent for provided resource""" + # First try, if we still have it + try: + return obj['metadata']['selfLink'] + except KeyError: + pass + + # If not, let's proceed with the path assembling. + try: + res_type = RESOURCE_MAP[obj['kind']] + except KeyError: + LOG.error('Unknown resource kind: %s', obj.get('kind')) + raise + + namespace = '' + if obj['metadata'].get('namespace'): + namespace = f"/namespaces/{obj['metadata']['namespace']}" + + try: + api = f"/apis/{obj['apiVersion']}" + if API_RE.match(obj['apiVersion']): + api = f"/api/{obj['apiVersion']}" + except KeyError: + LOG.error("Object doesn't have an apiVersion available: %s", obj) + raise + + return f"{api}{namespace}/{res_type}/{obj['metadata']['name']}" + + +def get_api_ver(path): + """Get apiVersion out of resource path. + + Path usually is something simillar to: + + /api/v1/namespaces/default/pods/pod-5bb648d658-55n76 + + in case of core resources, and: + + /apis/openstack.org/v1/namespaces/default/kuryrloadbalancers/lb-324 + + in case of custom resoures. + """ + if path.startswith('/api/'): + return path.split('/')[2] + + if path.startswith('/apis/'): + return '/'.join(path.split('/')[2:4]) + + raise ValueError('Provided path is not Kubernetes api path: %s', path) + def utf8_json_decoder(byte_data): """Deserializes the bytes into UTF-8 encoded JSON. diff --git a/kuryr_kubernetes/watcher.py b/kuryr_kubernetes/watcher.py index ce54abe96..39d176f7f 100644 --- a/kuryr_kubernetes/watcher.py +++ b/kuryr_kubernetes/watcher.py @@ -16,12 +16,13 @@ import sys import time +from oslo_config import cfg +from oslo_log import log as logging + from kuryr_kubernetes import clients from kuryr_kubernetes import exceptions from kuryr_kubernetes.handlers import health from kuryr_kubernetes import utils -from oslo_config import cfg -from oslo_log import log as logging LOG = logging.getLogger(__name__) CONF = cfg.CONF @@ -143,13 +144,6 @@ class Watcher(health.HealthHandler): return for resource in resources: - kind = response['kind'] - # Strip List from e.g. PodList. For some reason `.items` of a list - # returned from API doesn't have `kind` set. - if kind.endswith('List'): - kind = kind[:-4] - resource['kind'] = kind - event = { 'type': 'MODIFIED', 'object': resource,