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
This commit is contained in:
Roman Dobosz 2020-12-23 10:32:39 +01:00
parent 816ba2f8cd
commit e3ff9547a6
5 changed files with 184 additions and 11 deletions

View File

@ -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):

View File

@ -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'

View File

@ -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')

View File

@ -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.

View File

@ -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,