From fa269e3789d19f1335ae9207817203c6ad58cf42 Mon Sep 17 00:00:00 2001 From: Rodolfo Alonso Hernandez Date: Tue, 8 May 2018 17:42:08 +0100 Subject: Add new Kubernetes resource kind: "CustomResourceDefinition" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- yardstick/benchmark/contexts/kubernetes.py | 12 +++ yardstick/common/constants.py | 4 + yardstick/common/exceptions.py | 16 ++++ yardstick/common/kubernetes_utils.py | 39 +++++++- yardstick/orchestrator/kubernetes.py | 45 +++++++++ .../tests/unit/common/test_kubernetes_utils.py | 105 +++++++++++++++++++++ .../tests/unit/orchestrator/test_kubernetes.py | 27 ++++++ 7 files changed, 245 insertions(+), 3 deletions(-) create mode 100644 yardstick/tests/unit/common/test_kubernetes_utils.py diff --git a/yardstick/benchmark/contexts/kubernetes.py b/yardstick/benchmark/contexts/kubernetes.py index 6b00c0374..8cc5dc994 100644 --- a/yardstick/benchmark/contexts/kubernetes.py +++ b/yardstick/benchmark/contexts/kubernetes.py @@ -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) diff --git a/yardstick/common/constants.py b/yardstick/common/constants.py index f6e4ab7e9..954e59235 100644 --- a/yardstick/common/constants.py +++ b/yardstick/common/constants.py @@ -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' diff --git a/yardstick/common/exceptions.py b/yardstick/common/exceptions.py index 5f362b356..e370b92bf 100644 --- a/yardstick/common/exceptions.py +++ b/yardstick/common/exceptions.py @@ -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' diff --git a/yardstick/common/kubernetes_utils.py b/yardstick/common/kubernetes_utils.py index ee8e8edcd..a472b6de5 100644 --- a/yardstick/common/kubernetes_utils.py +++ b/yardstick/common/kubernetes_utils.py @@ -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: diff --git a/yardstick/orchestrator/kubernetes.py b/yardstick/orchestrator/kubernetes.py index 25adff7d4..637abd813 100644 --- a/yardstick/orchestrator/kubernetes.py +++ b/yardstick/orchestrator/kubernetes.py @@ -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 index 000000000..e264fd94b --- /dev/null +++ b/yardstick/tests/unit/common/test_kubernetes_utils.py @@ -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') diff --git a/yardstick/tests/unit/orchestrator/test_kubernetes.py b/yardstick/tests/unit/orchestrator/test_kubernetes.py index 99aca0426..50c6b2773 100644 --- a/yardstick/tests/unit/orchestrator/test_kubernetes.py +++ b/yardstick/tests/unit/orchestrator/test_kubernetes.py @@ -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') -- cgit 1.2.3-korg