Add new Kubernetes resource kind: "CustomResourceDefinition" 19/57219/8
authorRodolfo Alonso Hernandez <rodolfo.alonso.hernandez@intel.com>
Tue, 8 May 2018 16:42:08 +0000 (17:42 +0100)
committerRodolfo Alonso Hernandez <rodolfo.alonso.hernandez@intel.com>
Thu, 14 Jun 2018 07:15:35 +0000 (07:15 +0000)
Custom resource definition example in Kubernetes:
  apiVersion: "apiextensions.k8s.io/v1beta"
  kind: CustomResourceDefinition
  metadata:
    name: networks.kubernetes.com
  spec:
    group: kubernetes.com
    version: v1
    scope: Namespaced
    names:
      plural: networks
      singular: network
      kind: Network

Proposed Kubernetes context network definition:
  context:
    custom_resources:
      - name: network        # name of the resource (singular)
        version: v1          # optional, "v1" by default
        scope: Namespaced    # optional, "Namespaced" by default

From this definition, we will extract the Kubernetes parameters:
  - metadata.name: custom_resources.name + "s" + context_name + ".com"
  - spec.group: context_name + ".com"
  - spec.scope: custom_resources.scope
  - spec.version: custom_resources.version
  - spec.names.plural: custom_resources.name + "s"
  - spec.names.singular: custom_resources.name
  - spec.names.kind: custom_resources.name with first capital letter

[1] https://kubernetes.io/docs/concepts/api-extension/custom-resources/

JIRA: YARDSTICK-1163

Change-Id: If8980dc3f6ddf9c6949bf15be8011aa98482ddc9
Signed-off-by: Rodolfo Alonso Hernandez <rodolfo.alonso.hernandez@intel.com>
yardstick/benchmark/contexts/kubernetes.py
yardstick/common/constants.py
yardstick/common/exceptions.py
yardstick/common/kubernetes_utils.py
yardstick/orchestrator/kubernetes.py
yardstick/tests/unit/common/test_kubernetes_utils.py [new file with mode: 0644]
yardstick/tests/unit/orchestrator/test_kubernetes.py

index 6b00c03..8cc5dc9 100644 (file)
@@ -48,6 +48,7 @@ class KubernetesContext(Context):
         LOG.info('Creating ssh key')
         self._set_ssh_key()
 
+        self._create_crd()
         LOG.info('Launch containers')
         self._create_rcs()
         self._create_services()
@@ -61,6 +62,7 @@ class KubernetesContext(Context):
         self._delete_rcs()
         self._delete_pods()
         self._delete_services()
+        self._delete_crd()
 
         super(KubernetesContext, self).undeploy()
 
@@ -107,6 +109,16 @@ class KubernetesContext(Context):
     def _delete_pod(self, pod):
         k8s_utils.delete_pod(pod)
 
+    def _create_crd(self):
+        LOG.info('Create Custom Resource Definition elements')
+        for crd in self.template.crd:
+            crd.create()
+
+    def _delete_crd(self):
+        LOG.info('Delete Custom Resource Definition elements')
+        for crd in self.template.crd:
+            crd.delete()
+
     def _get_key_path(self):
         task_id = self.name.split('-')[-1]
         k = 'files/yardstick_key-{}'.format(task_id)
index f6e4ab7..954e592 100644 (file)
@@ -170,3 +170,7 @@ TESTSUITE_PRE = 'opnfv_'
 
 # OpenStack cloud default config parameters
 OS_CLOUD_DEFAULT_CONFIG = {'verify': False}
+
+# Kubernetes
+SCOPE_NAMESPACED = 'Namespaced'
+SCOPE_CLUSTER = 'Cluster'
index 5f362b3..e370b92 100644 (file)
@@ -14,6 +14,8 @@
 
 from oslo_utils import excutils
 
+from yardstick.common import constants
+
 
 class ProcessExecutionError(RuntimeError):
     def __init__(self, message, returncode):
@@ -195,10 +197,24 @@ class WaitTimeout(YardstickException):
     message = 'Wait timeout while waiting for condition'
 
 
+class KubernetesApiException(YardstickException):
+    message = ('Kubernetes API errors. Action: %(action)s, '
+               'resource: %(resource)s')
+
+
+class KubernetesConfigFileNotFound(YardstickException):
+    message = 'Config file (%s) not found' % constants.K8S_CONF_FILE
+
+
 class KubernetesTemplateInvalidVolumeType(YardstickException):
     message = 'No valid "volume" types present in %(volume)s'
 
 
+class KubernetesCRDObjectDefinitionError(YardstickException):
+    message = ('Kubernetes Custom Resource Definition Object error, missing '
+               'parameters: %(missing_parameters)s')
+
+
 class ScenarioCreateNetworkError(YardstickException):
     message = 'Create Neutron Network Scenario failed'
 
index ee8e8ed..a472b6d 100644 (file)
@@ -13,6 +13,8 @@ from kubernetes import config
 from kubernetes.client.rest import ApiException
 
 from yardstick.common import constants as consts
+from yardstick.common import exceptions
+
 
 LOG = logging.getLogger(__name__)
 LOG.setLevel(logging.DEBUG)
@@ -22,12 +24,18 @@ def get_core_api():     # pragma: no cover
     try:
         config.load_kube_config(config_file=consts.K8S_CONF_FILE)
     except IOError:
-        LOG.exception('config file not found')
-        raise
-
+        raise exceptions.KubernetesConfigFileNotFound()
     return client.CoreV1Api()
 
 
+def get_extensions_v1beta_api():
+    try:
+        config.load_kube_config(config_file=consts.K8S_CONF_FILE)
+    except IOError:
+        raise exceptions.KubernetesConfigFileNotFound()
+    return client.ApiextensionsV1beta1Api()
+
+
 def get_node_list(**kwargs):        # pragma: no cover
     core_v1_api = get_core_api()
     try:
@@ -187,6 +195,31 @@ def delete_config_map(name,
         raise
 
 
+def create_custom_resource_definition(body):
+    api = get_extensions_v1beta_api()
+    body_obj = client.V1beta1CustomResourceDefinition(
+        spec=body['spec'], metadata=body['metadata'])
+    try:
+        api.create_custom_resource_definition(body_obj)
+    except ValueError:
+        # NOTE(ralonsoh): bug in kubernetes-client/python 6.0.0
+        # https://github.com/kubernetes-client/python/issues/491
+        pass
+    except ApiException:
+        raise exceptions.KubernetesApiException(
+            action='create', resource='CustomResourceDefinition')
+
+
+def delete_custom_resource_definition(name):
+    api = get_extensions_v1beta_api()
+    body_obj = client.V1DeleteOptions()
+    try:
+        api.delete_custom_resource_definition(name, body_obj)
+    except ApiException:
+        raise exceptions.KubernetesApiException(
+            action='delete', resource='CustomResourceDefinition')
+
+
 def get_pod_list(namespace='default'):      # pragma: no cover
     core_v1_api = get_core_api()
     try:
index 25adff7..637abd8 100644 (file)
@@ -9,6 +9,7 @@
 
 import copy
 
+from yardstick.common import constants
 from yardstick.common import exceptions
 from yardstick.common import utils
 from yardstick.common import kubernetes_utils as k8s_utils
@@ -195,6 +196,47 @@ class ServiceObject(object):
         k8s_utils.delete_service(self.name)
 
 
+class CustomResourceDefinitionObject(object):
+
+    MANDATORY_PARAMETERS = {'name'}
+
+    def __init__(self, ctx_name, **kwargs):
+        if not self.MANDATORY_PARAMETERS.issubset(kwargs):
+            missing_parameters = ', '.join(
+                str(param) for param in
+                (self.MANDATORY_PARAMETERS - set(kwargs)))
+            raise exceptions.KubernetesCRDObjectDefinitionError(
+                missing_parameters=missing_parameters)
+
+        singular = kwargs['name']
+        plural = singular + 's'
+        kind = singular.title()
+        version = kwargs.get('version', 'v1')
+        scope = kwargs.get('scope', constants.SCOPE_NAMESPACED)
+        group = ctx_name + '.com'
+        self._name = metadata_name = plural + '.' + group
+
+        self._template = {
+            'metadata': {
+                'name': metadata_name
+            },
+            'spec': {
+                'group': group,
+                'version': version,
+                'scope': scope,
+                'names': {'plural': plural,
+                          'singular': singular,
+                          'kind': kind}
+            }
+        }
+
+    def create(self):
+        k8s_utils.create_custom_resource_definition(self._template)
+
+    def delete(self):
+        k8s_utils.delete_custom_resource_definition(self._name)
+
+
 class KubernetesTemplate(object):
 
     def __init__(self, name, context_cfg):
@@ -205,6 +247,7 @@ class KubernetesTemplate(object):
         """
         context_cfg = copy.deepcopy(context_cfg)
         servers_cfg = context_cfg.pop('servers', {})
+        crd_cfg = context_cfg.pop('custom_resources', [])
         self.name = name
         self.ssh_key = '{}-key'.format(name)
 
@@ -214,6 +257,8 @@ class KubernetesTemplate(object):
                                           **cfg)
                          for rc, cfg in servers_cfg.items()]
         self.service_objs = [ServiceObject(s) for s in self.rcs]
+        self.crd = [CustomResourceDefinitionObject(self.name, **crd)
+                    for crd in crd_cfg]
 
         self.pods = []
 
diff --git a/yardstick/tests/unit/common/test_kubernetes_utils.py b/yardstick/tests/unit/common/test_kubernetes_utils.py
new file mode 100644 (file)
index 0000000..e264fd9
--- /dev/null
@@ -0,0 +1,105 @@
+# Copyright (c) 2018 Intel Corporation
+#
+# 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.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import mock
+from kubernetes import client
+from kubernetes.client import rest
+from kubernetes import config
+
+from yardstick.common import constants
+from yardstick.common import exceptions
+from yardstick.common import kubernetes_utils
+from yardstick.tests.unit import base
+
+
+class GetExtensionsV1betaApiTestCase(base.BaseUnitTestCase):
+
+    @mock.patch.object(client, 'ApiextensionsV1beta1Api', return_value='api')
+    @mock.patch.object(config, 'load_kube_config')
+    def test_execute_correct(self, mock_load_kube_config, mock_api):
+        self.assertEqual('api', kubernetes_utils.get_extensions_v1beta_api())
+        mock_load_kube_config.assert_called_once_with(
+            config_file=constants.K8S_CONF_FILE)
+        mock_api.assert_called_once()
+
+    @mock.patch.object(config, 'load_kube_config')
+    def test_execute_exception(self, mock_load_kube_config):
+        mock_load_kube_config.side_effect = IOError
+        with self.assertRaises(exceptions.KubernetesConfigFileNotFound):
+            kubernetes_utils.get_extensions_v1beta_api()
+
+
+class CreateCustomResourceDefinitionTestCase(base.BaseUnitTestCase):
+
+    @mock.patch.object(client, 'V1beta1CustomResourceDefinition',
+                       return_value='crd_obj')
+    @mock.patch.object(kubernetes_utils, 'get_extensions_v1beta_api')
+    def test_execute_correct(self, mock_get_api, mock_crd):
+        mock_create_crd = mock.Mock()
+        mock_get_api.return_value = mock_create_crd
+        body = {'spec': 'fake_spec', 'metadata': 'fake_metadata'}
+
+        kubernetes_utils.create_custom_resource_definition(body)
+        mock_get_api.assert_called_once()
+        mock_crd.assert_called_once_with(spec='fake_spec',
+                                         metadata='fake_metadata')
+        mock_create_crd.create_custom_resource_definition.\
+            assert_called_once_with('crd_obj')
+
+    @mock.patch.object(client, 'V1beta1CustomResourceDefinition',
+                       return_value='crd_obj')
+    @mock.patch.object(kubernetes_utils, 'get_extensions_v1beta_api')
+    def test_execute_exception(self, mock_get_api, mock_crd):
+        mock_create_crd = mock.Mock()
+        mock_create_crd.create_custom_resource_definition.\
+            side_effect = rest.ApiException
+        mock_get_api.return_value = mock_create_crd
+        body = {'spec': 'fake_spec', 'metadata': 'fake_metadata'}
+
+        with self.assertRaises(exceptions.KubernetesApiException):
+            kubernetes_utils.create_custom_resource_definition(body)
+        mock_get_api.assert_called_once()
+        mock_crd.assert_called_once_with(spec='fake_spec',
+                                         metadata='fake_metadata')
+        mock_create_crd.create_custom_resource_definition.\
+            assert_called_once_with('crd_obj')
+
+
+class DeleteCustomResourceDefinitionTestCase(base.BaseUnitTestCase):
+
+    @mock.patch.object(client, 'V1DeleteOptions', return_value='del_obj')
+    @mock.patch.object(kubernetes_utils, 'get_extensions_v1beta_api')
+    def test_execute_correct(self, mock_get_api, mock_delobj):
+        mock_delete_crd = mock.Mock()
+        mock_get_api.return_value = mock_delete_crd
+
+        kubernetes_utils.delete_custom_resource_definition('name')
+        mock_get_api.assert_called_once()
+        mock_delobj.assert_called_once()
+        mock_delete_crd.delete_custom_resource_definition.\
+            assert_called_once_with('name', 'del_obj')
+
+    @mock.patch.object(client, 'V1DeleteOptions', return_value='del_obj')
+    @mock.patch.object(kubernetes_utils, 'get_extensions_v1beta_api')
+    def test_execute_exception(self, mock_get_api, mock_delobj):
+        mock_delete_crd = mock.Mock()
+        mock_delete_crd.delete_custom_resource_definition.\
+            side_effect = rest.ApiException
+        mock_get_api.return_value = mock_delete_crd
+
+        with self.assertRaises(exceptions.KubernetesApiException):
+            kubernetes_utils.delete_custom_resource_definition('name')
+        mock_delobj.assert_called_once()
+        mock_delete_crd.delete_custom_resource_definition.\
+            assert_called_once_with('name', 'del_obj')
index 99aca04..50c6b27 100644 (file)
@@ -270,3 +270,30 @@ class ContainerObjectTestCase(base.BaseUnitTestCase):
                     'volumeMounts': container_obj._create_volume_mounts(),
                     'securityContext': {'key': 'value'}}
         self.assertEqual(expected, container_obj.get_container_item())
+
+
+class CustomResourceDefinitionObjectTestCase(base.BaseUnitTestCase):
+
+    def test__init(self):
+        template = {
+            'metadata': {
+                'name': 'newcrds.ctx_name.com'
+            },
+            'spec': {
+                'group': 'ctx_name.com',
+                'version': 'v2',
+                'scope': 'scope',
+                'names': {'plural': 'newcrds',
+                          'singular': 'newcrd',
+                          'kind': 'Newcrd'}
+            }
+        }
+        crd_obj = kubernetes.CustomResourceDefinitionObject(
+            'ctx_name', name='newcrd', version='v2', scope='scope')
+        self.assertEqual('newcrds.ctx_name.com', crd_obj._name)
+        self.assertEqual(template, crd_obj._template)
+
+    def test__init_missing_parameter(self):
+        with self.assertRaises(exceptions.KubernetesCRDObjectDefinitionError):
+            kubernetes.CustomResourceDefinitionObject('ctx_name',
+                                                      noname='name')