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:
parent
816ba2f8cd
commit
e3ff9547a6
|
@ -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):
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue