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