From d3a6cdcf99a007671ee4e2e6982f0fe453cd476d Mon Sep 17 00:00:00 2001 From: Antoni Segura Puimedon Date: Fri, 23 Mar 2018 11:46:19 +0100 Subject: [PATCH] Kuryr-Controller: make handlers pluggable This patch introduces a new way for configuring which handlers the Kuryr controller should be using. This will allow people to use externally provided handlers as long as they are installed as entrypoints of the right namespace. Implements: blueprint kuryr-pluggable-handlers Change-Id: I52ce0ef00771c8587d7f7113cc5eb4839d1309a5 Signed-off-by: Antoni Segura Puimedon --- kuryr_kubernetes/config.py | 4 ++ kuryr_kubernetes/controller/handlers/lbaas.py | 2 + kuryr_kubernetes/controller/handlers/vif.py | 1 + kuryr_kubernetes/controller/service.py | 47 +++++++++++++++---- kuryr_kubernetes/handlers/k8s_base.py | 14 ++++-- .../controller/handlers/test_fake_handler.py | 24 ++++++++++ .../tests/unit/controller/test_service.py | 18 +++++++ ...e-handlers-pluggable-844475484771ffd6.yaml | 23 +++++++++ setup.cfg | 6 +++ 9 files changed, 127 insertions(+), 12 deletions(-) create mode 100644 kuryr_kubernetes/tests/unit/controller/handlers/test_fake_handler.py create mode 100644 releasenotes/notes/make-handlers-pluggable-844475484771ffd6.yaml diff --git a/kuryr_kubernetes/config.py b/kuryr_kubernetes/config.py index a858b9c49..fae5f814c 100644 --- a/kuryr_kubernetes/config.py +++ b/kuryr_kubernetes/config.py @@ -149,6 +149,10 @@ k8s_opts = [ cfg.IntOpt('watch_retry_timeout', help=_('Time (in seconds) the watcher retries watching for.'), default=60), + cfg.ListOpt('enabled_handlers', + help=_("The comma-separated handlers that should be " + "registered for watching in the pipeline."), + default=['vif', 'lb', 'lbaasspec']), ] neutron_defaults = [ diff --git a/kuryr_kubernetes/controller/handlers/lbaas.py b/kuryr_kubernetes/controller/handlers/lbaas.py index c78d804ed..7492e48a1 100644 --- a/kuryr_kubernetes/controller/handlers/lbaas.py +++ b/kuryr_kubernetes/controller/handlers/lbaas.py @@ -38,6 +38,7 @@ class LBaaSSpecHandler(k8s_base.ResourceEventHandler): """ OBJECT_KIND = k_const.K8S_OBJ_SERVICE + OBJECT_WATCH_PATH = "%s/%s" % (k_const.K8S_API_BASE, "services") def __init__(self): super(LBaaSSpecHandler, self).__init__() @@ -218,6 +219,7 @@ class LoadBalancerHandler(k8s_base.ResourceEventHandler): """ OBJECT_KIND = k_const.K8S_OBJ_ENDPOINTS + OBJECT_WATCH_PATH = "%s/%s" % (k_const.K8S_API_BASE, "endpoints") def __init__(self): super(LoadBalancerHandler, self).__init__() diff --git a/kuryr_kubernetes/controller/handlers/vif.py b/kuryr_kubernetes/controller/handlers/vif.py index fdc4680ce..45ec48820 100644 --- a/kuryr_kubernetes/controller/handlers/vif.py +++ b/kuryr_kubernetes/controller/handlers/vif.py @@ -38,6 +38,7 @@ class VIFHandler(k8s_base.ResourceEventHandler): """ OBJECT_KIND = constants.K8S_OBJ_POD + OBJECT_WATCH_PATH = "%s/%s" % (constants.K8S_API_BASE, "pods") def __init__(self): super(VIFHandler, self).__init__() diff --git a/kuryr_kubernetes/controller/service.py b/kuryr_kubernetes/controller/service.py index 0961871ec..f905e3336 100644 --- a/kuryr_kubernetes/controller/service.py +++ b/kuryr_kubernetes/controller/service.py @@ -16,22 +16,52 @@ import sys import os_vif +from oslo_config import cfg from oslo_log import log as logging from oslo_service import service +from stevedore.named import NamedExtensionManager from kuryr_kubernetes import clients from kuryr_kubernetes import config -from kuryr_kubernetes import constants -from kuryr_kubernetes.controller.handlers import lbaas as h_lbaas from kuryr_kubernetes.controller.handlers import pipeline as h_pipeline -from kuryr_kubernetes.controller.handlers import vif as h_vif from kuryr_kubernetes.controller.managers import health from kuryr_kubernetes import objects from kuryr_kubernetes import watcher + +CONF = cfg.CONF LOG = logging.getLogger(__name__) +def _handler_not_found(names): + LOG.exception('Handlers "%s" were not found.', names) + LOG.critical('Handlers "%s" were not found.', names) + raise SystemExit() + + +def _handler_not_loaded(manager, entrypoint, exception): + LOG.exception('Exception when loading handlers %s.', entrypoint) + LOG.critical('Handlers entrypoint "%s" failed to load due to %s.', + entrypoint, exception) + raise SystemExit() + + +def _load_kuryr_ctrlr_handlers(): + configured_handlers = CONF.kubernetes.enabled_handlers + LOG.info('Configured handlers: %s', configured_handlers) + handlers = NamedExtensionManager( + 'kuryr_kubernetes.controller.handlers', + configured_handlers, + invoke_on_load=True, + on_missing_entrypoints_callback=_handler_not_found, + on_load_failure_callback=_handler_not_loaded) + LOG.info('Loaded handlers: %s', handlers.names()) + ctrlr_handlers = [] + for handler in handlers.extensions: + ctrlr_handlers.append(handler.obj) + return ctrlr_handlers + + class KuryrK8sService(service.Service): """Kuryr-Kubernetes controller Service.""" @@ -42,12 +72,11 @@ class KuryrK8sService(service.Service): pipeline = h_pipeline.ControllerPipeline(self.tg) self.watcher = watcher.Watcher(pipeline, self.tg) self.health_manager = health.HealthServer() - # TODO(ivc): pluggable resource/handler registration - for resource in ["pods", "services", "endpoints"]: - self.watcher.add("%s/%s" % (constants.K8S_API_BASE, resource)) - pipeline.register(h_vif.VIFHandler()) - pipeline.register(h_lbaas.LBaaSSpecHandler()) - pipeline.register(h_lbaas.LoadBalancerHandler()) + + handlers = _load_kuryr_ctrlr_handlers() + for handler in handlers: + self.watcher.add(handler.get_watch_path()) + pipeline.register(handler) def start(self): LOG.info("Service '%s' starting", self.__class__.__name__) diff --git a/kuryr_kubernetes/handlers/k8s_base.py b/kuryr_kubernetes/handlers/k8s_base.py index 9312e1436..7be41ac04 100755 --- a/kuryr_kubernetes/handlers/k8s_base.py +++ b/kuryr_kubernetes/handlers/k8s_base.py @@ -34,9 +34,13 @@ def object_uid(event): class ResourceEventHandler(dispatch.EventConsumer, health.HealthHandler): """Base class for K8s event handlers. - Implementing classes should override the `OBJECT_KIND` attribute with a - valid Kubernetes object type name (e.g. 'Pod' or 'Namespace'; see [1] - for more details). + Implementing classes should override both `OBJECT_KIND` and + 'OBJECT_WATCH_PATH' attributes. + The `OBJECT_KIND` should be set to a valid Kubernetes object type + name (e.g. 'Pod' or 'Namespace'; see [1] for more details). + + The `OBJECT_WATCH_PATH` should point to object's watched path, + (e.g. for the 'Pod' case the OBJECT_WATCH_PATH should be '/api/v1/pods'). Implementing classes are expected to override any or all of the `on_added`, `on_present`, `on_modified`, `on_deleted` methods that would @@ -48,10 +52,14 @@ class ResourceEventHandler(dispatch.EventConsumer, health.HealthHandler): """ OBJECT_KIND = None + OBJECT_WATCH_PATH = None def __init__(self): super(ResourceEventHandler, self).__init__() + def get_watch_path(self): + return self.OBJECT_WATCH_PATH + @property def consumes(self): return {object_kind: self.OBJECT_KIND} diff --git a/kuryr_kubernetes/tests/unit/controller/handlers/test_fake_handler.py b/kuryr_kubernetes/tests/unit/controller/handlers/test_fake_handler.py new file mode 100644 index 000000000..94bae4811 --- /dev/null +++ b/kuryr_kubernetes/tests/unit/controller/handlers/test_fake_handler.py @@ -0,0 +1,24 @@ +# Copyright (c) 2018 RedHat, Inc. +# All Rights Reserved. +# +# 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. +from kuryr_kubernetes.handlers import k8s_base + + +class TestHandler(k8s_base.ResourceEventHandler): + + OBJECT_KIND = 'DUMMY' + OBJECT_WATCH_PATH = 'DUMMY_PATH' + + def __init__(self): + super(TestHandler, self).__init__() diff --git a/kuryr_kubernetes/tests/unit/controller/test_service.py b/kuryr_kubernetes/tests/unit/controller/test_service.py index 8ea63d79b..355aaae9c 100644 --- a/kuryr_kubernetes/tests/unit/controller/test_service.py +++ b/kuryr_kubernetes/tests/unit/controller/test_service.py @@ -17,6 +17,8 @@ import mock from kuryr_kubernetes.controller import service from kuryr_kubernetes.tests import base as test_base +from kuryr_kubernetes.tests.unit.controller.handlers import test_fake_handler +from oslo_config import cfg class TestControllerService(test_base.TestCase): @@ -39,3 +41,19 @@ class TestControllerService(test_base.TestCase): m_svc.assert_called() m_oslo_launch.assert_called() m_launcher.wait.assert_called() + + def test_check_test_handler(self): + cfg.CONF.set_override('enabled_handlers', ['test_handler'], + group='kubernetes') + handlers = service._load_kuryr_ctrlr_handlers() + for handler in handlers: + self.assertEqual(handler.get_watch_path(), + test_fake_handler.TestHandler.OBJECT_WATCH_PATH) + + @mock.patch('kuryr_kubernetes.controller.service._handler_not_found') + def test_handler_not_found(self, m_handler_not_found): + + cfg.CONF.set_override('enabled_handlers', ['fake_handler'], + group='kubernetes') + service._load_kuryr_ctrlr_handlers() + m_handler_not_found.assert_called() diff --git a/releasenotes/notes/make-handlers-pluggable-844475484771ffd6.yaml b/releasenotes/notes/make-handlers-pluggable-844475484771ffd6.yaml new file mode 100644 index 000000000..3747c408e --- /dev/null +++ b/releasenotes/notes/make-handlers-pluggable-844475484771ffd6.yaml @@ -0,0 +1,23 @@ +--- +features: + - | + Introduced a pluggable interface for the Kuryr controller handlers. + Each Controller handler associates itself with specific Kubernetes + object kind and is expected to process the events of the watched + Kubernetes API endpoints. + The pluggable handlers framework enable both using externally provided + handlers in Kuryr Controller and controlling which handlers + should be active. + + To control which Kuryr Controller handlers should be active, the selected + handlers need to be included at the kuryr.conf at the 'kubernetes' + section. + If not specified, Kuryr Controller will run the default handlers. + For example, to enable only the 'vif' controller handler we should set + the following at kuryr.conf: + + .. code-block:: ini + + [kubernetes] + enabled_handlers=vif + diff --git a/setup.cfg b/setup.cfg index 09aa811e5..7ba1d9e6f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -75,6 +75,12 @@ kuryr_kubernetes.controller.drivers.vif_pool = nested = kuryr_kubernetes.controller.drivers.vif_pool:NestedVIFPool multi_pool = kuryr_kubernetes.controller.drivers.vif_pool:MultiVIFPool +kuryr_kubernetes.controller.handlers = + vif = kuryr_kubernetes.controller.handlers.vif:VIFHandler + lbaasspec = kuryr_kubernetes.controller.handlers.lbaas:LBaaSSpecHandler + lb = kuryr_kubernetes.controller.handlers.lbaas:LoadBalancerHandler + test_handler = kuryr_kubernetes.tests.unit.controller.handlers.test_fake_handler:TestHandler + [files] packages = kuryr_kubernetes