From d5902e8fed1560cb2ac56a3e24ce57cd5af603b2 Mon Sep 17 00:00:00 2001 From: Yossi Boaron Date: Wed, 24 Jan 2018 15:26:08 +0200 Subject: [PATCH] OCP-Router: OCP-Route and Ingress LBaaS handlers This is the third patch of the Ingress Controller capability. This patch implements OCP-Route and Ingress LBaaS handlers. Those handlers should retrieve the L7 LB details from the Ingress controller and update L7 policy/rules and pool/members upon changes in OCP-route and k8S-endpoint resources. Please follow the instructions below to verify OCP-Router functionality: https://docs.google.com/document/d/1c3mfBToBbWlwFcw3S8fr7pQZb5_YZqFYdlG1HqaQPkQ/edit?usp=sharing Implements: blueprint openshift-router-support Change-Id: Ibfb6cda6dde9613ad31859d38235be031ade0639 --- kuryr_kubernetes/constants.py | 4 + .../controller/handlers/ingress_lbaas.py | 212 +++++++++ kuryr_kubernetes/objects/lbaas.py | 32 ++ kuryr_kubernetes/objects/route.py | 43 ++ kuryr_kubernetes/platform/__init__.py | 0 kuryr_kubernetes/platform/constants.py | 17 + kuryr_kubernetes/platform/ocp/__init__.py | 0 .../platform/ocp/controller/__init__.py | 0 .../ocp/controller/handlers/__init__.py | 0 .../platform/ocp/controller/handlers/route.py | 255 +++++++++++ .../controller/handlers/test_ingress_lbaas.py | 186 ++++++++ .../unit/controller/handlers/test_lbaas.py | 5 +- .../tests/unit/platform/ocp/__init__.py | 0 .../ocp/controller/handlers/__init__.py | 0 .../ocp/controller/handlers/test_route.py | 403 ++++++++++++++++++ ...shift-router-support-5f28108b39a2826f.yaml | 15 + setup.cfg | 2 + 17 files changed, 1173 insertions(+), 1 deletion(-) create mode 100644 kuryr_kubernetes/controller/handlers/ingress_lbaas.py create mode 100644 kuryr_kubernetes/objects/route.py create mode 100644 kuryr_kubernetes/platform/__init__.py create mode 100644 kuryr_kubernetes/platform/constants.py create mode 100644 kuryr_kubernetes/platform/ocp/__init__.py create mode 100644 kuryr_kubernetes/platform/ocp/controller/__init__.py create mode 100644 kuryr_kubernetes/platform/ocp/controller/handlers/__init__.py create mode 100644 kuryr_kubernetes/platform/ocp/controller/handlers/route.py create mode 100644 kuryr_kubernetes/tests/unit/controller/handlers/test_ingress_lbaas.py create mode 100644 kuryr_kubernetes/tests/unit/platform/ocp/__init__.py create mode 100644 kuryr_kubernetes/tests/unit/platform/ocp/controller/handlers/__init__.py create mode 100644 kuryr_kubernetes/tests/unit/platform/ocp/controller/handlers/test_route.py create mode 100644 releasenotes/notes/bp-openshift-router-support-5f28108b39a2826f.yaml diff --git a/kuryr_kubernetes/constants.py b/kuryr_kubernetes/constants.py index 2ea28691a..beadbcd6d 100644 --- a/kuryr_kubernetes/constants.py +++ b/kuryr_kubernetes/constants.py @@ -31,6 +31,10 @@ K8S_ANNOTATION_VIF = K8S_ANNOTATION_PREFIX + '-vif' K8S_ANNOTATION_LBAAS_SPEC = K8S_ANNOTATION_PREFIX + '-lbaas-spec' K8S_ANNOTATION_LBAAS_STATE = K8S_ANNOTATION_PREFIX + '-lbaas-state' K8S_ANNOTATION_NET_CRD = K8S_ANNOTATION_PREFIX + '-net-crd' +K8S_ANNOTATION_LBAAS_RT_STATE = K8S_ANNOTATION_PREFIX + '-lbaas-route-state' +K8S_ANNOTATION_LBAAS_RT_NOTIF = K8S_ANNOTATION_PREFIX + '-lbaas-route-notif' +K8S_ANNOTATION_ROUTE_STATE = K8S_ANNOTATION_PREFIX + '-route-state' +K8S_ANNOTATION_ROUTE_SPEC = K8S_ANNOTATION_PREFIX + '-route-spec' K8S_OS_VIF_NOOP_PLUGIN = "noop" diff --git a/kuryr_kubernetes/controller/handlers/ingress_lbaas.py b/kuryr_kubernetes/controller/handlers/ingress_lbaas.py new file mode 100644 index 000000000..699dc7b2c --- /dev/null +++ b/kuryr_kubernetes/controller/handlers/ingress_lbaas.py @@ -0,0 +1,212 @@ +# 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 oslo_log import log as logging +from oslo_serialization import jsonutils + +from kuryr_kubernetes import clients +from kuryr_kubernetes import config +from kuryr_kubernetes import constants as k_const +from kuryr_kubernetes.controller.drivers import base as drv_base +from kuryr_kubernetes.controller.handlers import lbaas as h_lbaas +from kuryr_kubernetes.controller.ingress import ingress_ctl +from kuryr_kubernetes.objects import lbaas as obj_lbaas + +LOG = logging.getLogger(__name__) + + +class IngressLoadBalancerHandler(h_lbaas.LoadBalancerHandler): + """IngressLoadBalancerHandler handles K8s Endpoints events. + + IngressLoadBalancerHandler handles K8s Endpoints events and tracks + changes in LBaaSServiceSpec to update Ingress Controller + L7 router accordingly. + """ + + OBJECT_KIND = k_const.K8S_OBJ_ENDPOINTS + OBJECT_WATCH_PATH = "%s/%s" % (k_const.K8S_API_BASE, "endpoints") + + def __init__(self): + super(IngressLoadBalancerHandler, self).__init__() + self._drv_lbaas = drv_base.LBaaSDriver.get_instance() + self._l7_router = None + + def _should_ignore(self, endpoints, lbaas_spec): + return not(lbaas_spec and + self._has_pods(endpoints)) + + def on_present(self, endpoints): + if not self._l7_router: + ing_ctl = ingress_ctl.IngressCtrlr.get_instance() + self._l7_router, listener = ing_ctl.get_router_and_listener() + if not self._l7_router: + LOG.info("No L7 router found - do nothing") + return + + lbaas_spec = self._get_lbaas_spec(endpoints) + if self._should_ignore(endpoints, lbaas_spec): + return + + pool_name = self._drv_lbaas.get_loadbalancer_pool_name( + self._l7_router, endpoints['metadata']['namespace'], + endpoints['metadata']['name']) + pool = self._drv_lbaas.get_pool_by_name(pool_name, + self._l7_router.project_id) + if not pool: + if self._get_lbaas_route_state(endpoints): + self._set_lbaas_route_state(endpoints, None) + LOG.debug("L7 routing: no route defined for service " + ":%s - do nothing", endpoints['metadata']['name']) + else: + # pool was found in L7 router LB ,verify that members are up2date + lbaas_route_state = self._get_lbaas_route_state(endpoints) + if not lbaas_route_state: + lbaas_route_state = obj_lbaas.LBaaSRouteState() + lbaas_route_state.pool = pool + if self._sync_lbaas_route_members(endpoints, + lbaas_route_state, lbaas_spec): + self._set_lbaas_route_state(endpoints, lbaas_route_state) + self._clear_route_notification(endpoints) + + def on_deleted(self, endpoints): + if not self._l7_router: + LOG.info("No L7 router found - do nothing") + return + + lbaas_route_state = self._get_lbaas_route_state(endpoints) + if not lbaas_route_state: + return + self._remove_unused_route_members(endpoints, lbaas_route_state, + obj_lbaas.LBaaSServiceSpec()) + + def _sync_lbaas_route_members(self, endpoints, + lbaas_route_state, lbaas_spec): + changed = False + if self._remove_unused_route_members( + endpoints, lbaas_route_state, lbaas_spec): + changed = True + + if self._add_new_route_members(endpoints, lbaas_route_state): + changed = True + + return changed + + def _add_new_route_members(self, endpoints, lbaas_route_state): + changed = False + + current_targets = {(str(m.ip), m.port) + for m in lbaas_route_state.members} + + for subset in endpoints.get('subsets', []): + subset_ports = subset.get('ports', []) + for subset_address in subset.get('addresses', []): + try: + target_ip = subset_address['ip'] + target_ref = subset_address['targetRef'] + if target_ref['kind'] != k_const.K8S_OBJ_POD: + continue + except KeyError: + continue + for subset_port in subset_ports: + target_port = subset_port['port'] + if (target_ip, target_port) in current_targets: + continue + + # TODO(apuimedo): Do not pass subnet_id at all when in + # L3 mode once old neutron-lbaasv2 is not supported, as + # octavia does not require it + if (config.CONF.octavia_defaults.member_mode == + k_const.OCTAVIA_L2_MEMBER_MODE): + member_subnet_id = self._get_pod_subnet(target_ref, + target_ip) + else: + # We use the service subnet id so that the connectivity + # from VIP to pods happens in layer 3 mode, i.e., + # routed. + member_subnet_id = self._l7_router.subnet_id + member = self._drv_lbaas.ensure_member( + loadbalancer=self._l7_router, + pool=lbaas_route_state.pool, + subnet_id=member_subnet_id, + ip=target_ip, + port=target_port, + target_ref_namespace=target_ref['namespace'], + target_ref_name=target_ref['name']) + + lbaas_route_state.members.append(member) + changed = True + + return changed + + def _remove_unused_route_members( + self, endpoints, lbaas_route_state, lbaas_spec): + spec_port_names = {p.name for p in lbaas_spec.ports} + current_targets = {(a['ip'], p['port']) + for s in endpoints['subsets'] + for a in s['addresses'] + for p in s['ports'] + if p.get('name') in spec_port_names} + removed_ids = set() + for member in lbaas_route_state.members: + if (str(member.ip), member.port) in current_targets: + continue + self._drv_lbaas.release_member(self._l7_router, member) + removed_ids.add(member.id) + if removed_ids: + lbaas_route_state.members = [ + m for m in lbaas_route_state.members + if m.id not in removed_ids] + return bool(removed_ids) + + def _set_lbaas_route_state(self, endpoints, route_state): + if route_state is None: + LOG.debug("Removing LBaaSRouteState annotation: %r", route_state) + annotation = None + else: + route_state.obj_reset_changes(recursive=True) + LOG.debug("Setting LBaaSRouteState annotation: %r", route_state) + annotation = jsonutils.dumps(route_state.obj_to_primitive(), + sort_keys=True) + k8s = clients.get_kubernetes_client() + k8s.annotate(endpoints['metadata']['selfLink'], + {k_const.K8S_ANNOTATION_LBAAS_RT_STATE: annotation}, + resource_version=endpoints['metadata']['resourceVersion']) + + def _get_lbaas_route_state(self, endpoints): + try: + annotations = endpoints['metadata']['annotations'] + annotation = annotations[k_const.K8S_ANNOTATION_LBAAS_RT_STATE] + except KeyError: + return None + obj_dict = jsonutils.loads(annotation) + obj = obj_lbaas.LBaaSRouteState.obj_from_primitive(obj_dict) + LOG.debug("Got LBaaSRouteState from annotation: %r", obj) + return obj + + def _clear_route_notification(self, endpoints): + try: + annotations = endpoints['metadata']['annotations'] + annotation = annotations[ + k_const.K8S_ANNOTATION_LBAAS_RT_NOTIF] + except KeyError: + return + + LOG.debug("Removing LBaaSRouteNotifier annotation") + annotation = None + k8s = clients.get_kubernetes_client() + k8s.annotate( + endpoints['metadata']['selfLink'], + {k_const.K8S_ANNOTATION_LBAAS_RT_NOTIF: annotation}, + resource_version=endpoints['metadata']['resourceVersion']) diff --git a/kuryr_kubernetes/objects/lbaas.py b/kuryr_kubernetes/objects/lbaas.py index 73cd8c284..014b4e55c 100644 --- a/kuryr_kubernetes/objects/lbaas.py +++ b/kuryr_kubernetes/objects/lbaas.py @@ -167,3 +167,35 @@ class LBaaSL7Rule(k_obj.KuryrK8sObjectBase): 'type': obj_fields.StringField(nullable=True), 'value': obj_fields.StringField(nullable=True), } + + +@obj_base.VersionedObjectRegistry.register +class LBaaSRouteState(k_obj.KuryrK8sObjectBase): + VERSION = '1.0' + + fields = { + 'members': obj_fields.ListOfObjectsField(LBaaSMember.__name__, + default=[]), + 'pool': obj_fields.ObjectField(LBaaSPool.__name__, + nullable=True, default=None), + } + + +@obj_base.VersionedObjectRegistry.register +class LBaaSRouteNotifEntry(k_obj.KuryrK8sObjectBase): + VERSION = '1.0' + + fields = { + 'route_id': obj_fields.UUIDField(), + 'msg': obj_fields.StringField(), + } + + +@obj_base.VersionedObjectRegistry.register +class LBaaSRouteNotifier(k_obj.KuryrK8sObjectBase): + VERSION = '1.0' + + fields = { + 'routes': obj_fields.ListOfObjectsField( + LBaaSRouteNotifEntry.__name__, default=[]), + } diff --git a/kuryr_kubernetes/objects/route.py b/kuryr_kubernetes/objects/route.py new file mode 100644 index 000000000..fbe50265c --- /dev/null +++ b/kuryr_kubernetes/objects/route.py @@ -0,0 +1,43 @@ +# 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.objects import base as k_obj +from kuryr_kubernetes.objects import lbaas as lbaas_obj +from oslo_versionedobjects import base as obj_base +from oslo_versionedobjects import fields as obj_fields + + +@obj_base.VersionedObjectRegistry.register +class RouteState(k_obj.KuryrK8sObjectBase): + VERSION = '1.0' + fields = { + 'router_pool': obj_fields.ObjectField( + lbaas_obj.LBaaSPool.__name__, nullable=True, default=None), + 'l7_policy': obj_fields.ObjectField( + lbaas_obj.LBaaSL7Policy.__name__, nullable=True, default=None), + 'h_l7_rule': obj_fields.ObjectField( + lbaas_obj.LBaaSL7Rule.__name__, nullable=True, default=None), + 'p_l7_rule': obj_fields.ObjectField( + lbaas_obj.LBaaSL7Rule.__name__, nullable=True, default=None), + } + + +@obj_base.VersionedObjectRegistry.register +class RouteSpec(k_obj.KuryrK8sObjectBase): + VERSION = '1.0' + fields = { + 'host': obj_fields.StringField(nullable=True, default=None), + 'path': obj_fields.StringField(nullable=True, default=None), + 'to_service': obj_fields.StringField(nullable=True, default=None), + } diff --git a/kuryr_kubernetes/platform/__init__.py b/kuryr_kubernetes/platform/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/kuryr_kubernetes/platform/constants.py b/kuryr_kubernetes/platform/constants.py new file mode 100644 index 000000000..d545dd17f --- /dev/null +++ b/kuryr_kubernetes/platform/constants.py @@ -0,0 +1,17 @@ +# 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. + +OCP_API_BASE = '/oapi/v1' +OCP_OBJ_ROUTE = 'Route' diff --git a/kuryr_kubernetes/platform/ocp/__init__.py b/kuryr_kubernetes/platform/ocp/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/kuryr_kubernetes/platform/ocp/controller/__init__.py b/kuryr_kubernetes/platform/ocp/controller/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/kuryr_kubernetes/platform/ocp/controller/handlers/__init__.py b/kuryr_kubernetes/platform/ocp/controller/handlers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/kuryr_kubernetes/platform/ocp/controller/handlers/route.py b/kuryr_kubernetes/platform/ocp/controller/handlers/route.py new file mode 100644 index 000000000..828352738 --- /dev/null +++ b/kuryr_kubernetes/platform/ocp/controller/handlers/route.py @@ -0,0 +1,255 @@ +# Copyright (c) 2017 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.lib._i18n import _ + +from kuryr_kubernetes import clients +from kuryr_kubernetes import constants as k_const +from kuryr_kubernetes.controller.drivers import base as drv_base +from kuryr_kubernetes.controller.ingress import ingress_ctl +from kuryr_kubernetes import exceptions as k_exc +from kuryr_kubernetes.handlers import k8s_base +from kuryr_kubernetes.objects import lbaas as obj_lbaas +from kuryr_kubernetes.objects import route as obj_route +from kuryr_kubernetes.platform import constants as ocp_const +from oslo_log import log as logging +from oslo_serialization import jsonutils + +LOG = logging.getLogger(__name__) + + +class OcpRouteHandler(k8s_base.ResourceEventHandler): + """OcpRouteHandler handles OCP route events. + + An OpenShift route allows service to be externally-reachable via host name. + This host name is then used to route traffic to the service. + The OcpRouteHandler is responsible for processing all route resource + events. + + """ + + OBJECT_KIND = ocp_const.OCP_OBJ_ROUTE + OBJECT_WATCH_PATH = "%s/%s" % (ocp_const.OCP_API_BASE, "routes") + + def __init__(self): + self._drv_lbaas = drv_base.LBaaSDriver.get_instance() + self._l7_router = None + self._l7_router_listeners = None + + def on_present(self, route): + if not self._l7_router or not self._l7_router_listeners: + ing_ctl = ingress_ctl.IngressCtrlr.get_instance() + self._l7_router, self._l7_router_listeners = ( + ing_ctl.get_router_and_listener()) + if not self._l7_router or not self._l7_router_listeners: + LOG.info("No L7 router found - do nothing") + return + + route_spec = self._get_route_spec(route) + if not route_spec: + route_spec = obj_route.RouteSpec() + + if self._should_ignore(route, route_spec): + return + route_state = self._get_route_state(route) + if not route_state: + route_state = obj_route.RouteState() + + self._sync_router_pool(route, route_spec, route_state) + self._sync_l7_policy(route, route_spec, route_state) + self._sync_host_l7_rule(route, route_spec, route_state) + self._sync_path_l7_rule(route, route_spec, route_state) + + self._set_route_state(route, route_state) + self._set_route_spec(route, route_spec) + self._send_route_notification_to_ep( + route, route_spec.to_service) + + def _get_endpoints_link_by_route(self, route_link, ep_name): + route_link = route_link.replace( + ocp_const.OCP_API_BASE, k_const.K8S_API_BASE) + link_parts = route_link.split('/') + if link_parts[-2] != 'routes': + raise k_exc.IntegrityError(_( + "Unsupported route link: %(link)s") % { + 'link': route_link}) + link_parts[-2] = 'endpoints' + link_parts[-1] = ep_name + return "/".join(link_parts) + + def _send_route_notification_to_ep(self, route, ep_name): + route_link = route['metadata']['selfLink'] + ep_link = self._get_endpoints_link_by_route(route_link, ep_name) + k8s = clients.get_kubernetes_client() + try: + k8s.get(ep_link) + except k_exc.K8sClientException: + LOG.debug("Failed to get EP link : %s", ep_link) + return + + route_notifier = obj_lbaas.LBaaSRouteNotifier() + route_notifier.routes.append( + obj_lbaas.LBaaSRouteNotifEntry( + route_id=route['metadata']['uid'], msg='RouteChanged')) + route_notifier.obj_reset_changes(recursive=True) + LOG.debug("Setting LBaaSRouteNotifier annotation: %r", route_notifier) + annotation = jsonutils.dumps(route_notifier.obj_to_primitive(), + sort_keys=True) + k8s.annotate( + ep_link, + {k_const.K8S_ANNOTATION_LBAAS_RT_NOTIF: annotation}, + resource_version=route['metadata']['resourceVersion']) + + def _should_ignore(self, route, route_spec): + spec = route['spec'] + return ((not self._l7_router) + or + ((spec.get('host') == route_spec.host) and + (spec.get('path') == route_spec.path) and + (spec['to'].get('name') == route_spec.to_service))) + + def on_deleted(self, route): + if not self._l7_router: + LOG.info("No L7 router found - do nothing") + return + + route_state = self._get_route_state(route) + if not route_state: + return + # NOTE(yboaron): deleting l7policy deletes also l7rules + if route_state.l7_policy: + self._drv_lbaas.release_l7_policy( + self._l7_router, route_state.l7_policy) + + if route_state.router_pool: + if self._drv_lbaas.is_pool_used_by_other_l7policies( + route_state.l7_policy, route_state.router_pool): + LOG.debug("Can't delete pool (pointed by another route)") + else: + self._drv_lbaas.release_pool( + self._l7_router, route_state.router_pool) + # no more routes pointing to this pool/ep - update ep + spec = route['spec'] + self._send_route_notification_to_ep( + route, spec['to'].get('name')) + + def _sync_router_pool(self, route, route_spec, route_state): + if route_state.router_pool: + return + + pool_name = self._drv_lbaas.get_loadbalancer_pool_name( + self._l7_router, route['metadata']['namespace'], + route['spec']['to']['name']) + pool = self._drv_lbaas.get_pool_by_name( + pool_name, self._l7_router.project_id) + if not pool: + pool = self._drv_lbaas.ensure_pool_attached_to_lb( + self._l7_router, route['metadata']['namespace'], + route['spec']['to']['name'], protocol='HTTP') + + route_state.router_pool = pool + route_spec.to_service = route['spec']['to']['name'] + + def _sync_l7_policy(self, route, route_spec, route_state): + if route_state.l7_policy: + return + # TBD , take care of listener HTTPS + listener = self._l7_router_listeners[k_const.KURYR_L7_ROUTER_HTTP_PORT] + + route_state.l7_policy = self._drv_lbaas.ensure_l7_policy( + route['metadata']['namespace'], route['metadata']['name'], + self._l7_router, route_state.router_pool, listener.id) + + def _sync_host_l7_rule(self, route, route_spec, route_state): + if route_spec.host == route['spec']['host']: + return + if not route_spec.host: + route_state.h_l7_rule = self._drv_lbaas.ensure_l7_rule( + self._l7_router, route_state.l7_policy, + 'EQUAL_TO', 'HOST_NAME', route['spec']['host']) + else: + self._drv_lbaas.update_l7_rule( + route_state.h_l7_rule, route['spec']['host']) + route_state.h_l7_rule.value = route['spec']['host'] + + route_spec.host = route['spec']['host'] + + def _sync_path_l7_rule(self, route, route_spec, route_state): + if route_spec.path == route['spec'].get('path'): + return + if not route_spec.path: + route_state.p_l7_rule = self._drv_lbaas.ensure_l7_rule( + self._l7_router, route_state.l7_policy, + 'EQUAL_TO', 'PATH', route['spec']['path']) + else: + if route['spec']['path']: + self._drv_lbaas.update_l7_rule( + route_state.p_l7_rule, route['spec']['path']) + route_state.p_l7_rule.value = route['spec']['path'] + else: + self._drv_lbaas.release_l7_rule(route_state.p_l7_rule) + route_state.p_l7_rule = None + + route_spec.path = route['spec']['path'] + + def _get_route_spec(self, route): + try: + annotations = route['metadata']['annotations'] + annotation = annotations[k_const.K8S_ANNOTATION_ROUTE_SPEC] + except KeyError: + return obj_route.RouteSpec() + obj_dict = jsonutils.loads(annotation) + obj = obj_route.RouteSpec.obj_from_primitive(obj_dict) + LOG.debug("Got RouteSpec from annotation: %r", obj) + return obj + + def _set_route_spec(self, route, route_spec): + if route_spec is None: + LOG.debug("Removing RouteSpec annotation: %r", route_spec) + annotation = None + else: + route_spec.obj_reset_changes(recursive=True) + LOG.debug("Setting RouteSpec annotation: %r", route_spec) + annotation = jsonutils.dumps(route_spec.obj_to_primitive(), + sort_keys=True) + k8s = clients.get_kubernetes_client() + k8s.annotate(route['metadata']['selfLink'], + {k_const.K8S_ANNOTATION_ROUTE_SPEC: annotation}, + resource_version=route['metadata']['resourceVersion']) + + def _get_route_state(self, route): + try: + annotations = route['metadata']['annotations'] + annotation = annotations[k_const.K8S_ANNOTATION_ROUTE_STATE] + except KeyError: + return obj_route.RouteState() + obj_dict = jsonutils.loads(annotation) + obj = obj_route.RouteState.obj_from_primitive(obj_dict) + LOG.debug("Got RouteState from annotation: %r", obj) + return obj + + def _set_route_state(self, route, route_state): + if route_state is None: + LOG.debug("Removing RouteState annotation: %r", route_state) + annotation = None + else: + route_state.obj_reset_changes(recursive=True) + LOG.debug("Setting RouteState annotation: %r", route_state) + annotation = jsonutils.dumps(route_state.obj_to_primitive(), + sort_keys=True) + k8s = clients.get_kubernetes_client() + k8s.annotate(route['metadata']['selfLink'], + {k_const.K8S_ANNOTATION_ROUTE_STATE: annotation}, + resource_version=route['metadata']['resourceVersion']) diff --git a/kuryr_kubernetes/tests/unit/controller/handlers/test_ingress_lbaas.py b/kuryr_kubernetes/tests/unit/controller/handlers/test_ingress_lbaas.py new file mode 100644 index 000000000..db3613b69 --- /dev/null +++ b/kuryr_kubernetes/tests/unit/controller/handlers/test_ingress_lbaas.py @@ -0,0 +1,186 @@ +# 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. + +import mock +from oslo_utils import uuidutils + +from kuryr_kubernetes.controller.handlers import ingress_lbaas as h_ing_lbaas +from kuryr_kubernetes.objects import lbaas as obj_lbaas +from kuryr_kubernetes.tests.unit.controller.handlers import \ + test_lbaas as t_lbaas + + +class TestIngressLoadBalancerHandler(t_lbaas.TestLoadBalancerHandler): + + @mock.patch('kuryr_kubernetes.controller.drivers.base' + '.LBaaSDriver.get_instance') + def test_init(self, m_get_drv_lbaas): + m_get_drv_lbaas.return_value = mock.sentinel.drv_lbaas + + handler = h_ing_lbaas.IngressLoadBalancerHandler() + + self.assertEqual(mock.sentinel.drv_lbaas, handler._drv_lbaas) + + def test_on_present_no_ing_ctrlr(self): + endpoints = mock.sentinel.endpoints + + m_handler = mock.Mock(spec=h_ing_lbaas.IngressLoadBalancerHandler) + m_handler._l7_router = None + h_ing_lbaas.IngressLoadBalancerHandler.on_present(m_handler, endpoints) + + m_handler._get_lbaas_spec.assert_not_called() + m_handler._should_ignore.assert_not_called() + + def test_should_ignore(self): + endpoints = mock.sentinel.endpoints + lbaas_spec = mock.sentinel.lbaas_spec + + m_handler = mock.Mock(spec=h_ing_lbaas.IngressLoadBalancerHandler) + m_handler._has_pods.return_value = False + + ret = h_ing_lbaas.IngressLoadBalancerHandler._should_ignore( + m_handler, endpoints, lbaas_spec) + self.assertEqual(True, ret) + + m_handler._has_pods.assert_called_once_with(endpoints) + + def test_should_ignore_with_pods(self): + endpoints = mock.sentinel.endpoints + lbaas_spec = mock.sentinel.lbaas_spec + + m_handler = mock.Mock(spec=h_ing_lbaas.IngressLoadBalancerHandler) + m_handler._has_pods.return_value = True + + ret = h_ing_lbaas.IngressLoadBalancerHandler._should_ignore( + m_handler, endpoints, lbaas_spec) + self.assertEqual(False, ret) + + m_handler._has_pods.assert_called_once_with(endpoints) + + def _generate_route_state(self, vip, targets, project_id, subnet_id): + name = 'DUMMY_NAME' + drv = t_lbaas.FakeLBaaSDriver() + lb = drv.ensure_loadbalancer( + name, project_id, subnet_id, vip, None, 'ClusterIP') + pool = drv.ensure_pool_attached_to_lb(lb, 'namespace', + 'svc_name', 'HTTP') + + members = {} + for ip, (listen_port, target_port) in targets.items(): + members.setdefault((ip, listen_port, target_port), + drv.ensure_member(lb, pool, + subnet_id, ip, + target_port, None, None)) + return obj_lbaas.LBaaSRouteState( + pool=pool, + members=list(members.values())) + + def _sync_route_members_impl(self, m_get_drv_lbaas, m_get_drv_project, + m_get_drv_subnets, subnet_id, project_id, + endpoints, state, spec): + m_drv_lbaas = mock.Mock(wraps=t_lbaas.FakeLBaaSDriver()) + m_drv_project = mock.Mock() + m_drv_project.get_project.return_value = project_id + m_drv_subnets = mock.Mock() + m_drv_subnets.get_subnets.return_value = { + subnet_id: mock.sentinel.subnet} + m_get_drv_lbaas.return_value = m_drv_lbaas + m_get_drv_project.return_value = m_drv_project + m_get_drv_subnets.return_value = m_drv_subnets + + handler = h_ing_lbaas.IngressLoadBalancerHandler() + + handler._l7_router = t_lbaas.FakeLBaaSDriver().ensure_loadbalancer( + name='L7_Router', + project_id=project_id, + subnet_id=subnet_id, + ip='1.2.3.4', + security_groups_ids=None, + service_type='ClusterIP') + + with mock.patch.object(handler, '_get_pod_subnet') as m_get_pod_subnet: + m_get_pod_subnet.return_value = subnet_id + handler._sync_lbaas_route_members(endpoints, state, spec) + + observed_targets = sorted( + (str(member.ip), ( + member.port, + member.port)) + for member in state.members) + return observed_targets + + @mock.patch('kuryr_kubernetes.controller.drivers.base' + '.PodSubnetsDriver.get_instance') + @mock.patch('kuryr_kubernetes.controller.drivers.base' + '.PodProjectDriver.get_instance') + @mock.patch('kuryr_kubernetes.controller.drivers.base' + '.LBaaSDriver.get_instance') + def test__sync_lbaas_route_members(self, m_get_drv_lbaas, + m_get_drv_project, m_get_drv_subnets): + project_id = uuidutils.generate_uuid() + subnet_id = uuidutils.generate_uuid() + current_ip = '1.1.1.1' + current_targets = { + '1.1.1.101': (1001, 1001), + '1.1.1.111': (1001, 1001), + '1.1.1.201': (2001, 2001)} + expected_ip = '2.2.2.2' + expected_targets = { + '2.2.2.101': (1201, 1201), + '2.2.2.111': (1201, 1201), + '2.2.2.201': (2201, 2201)} + endpoints = self._generate_endpoints(expected_targets) + state = self._generate_route_state( + current_ip, current_targets, project_id, subnet_id) + spec = self._generate_lbaas_spec(expected_ip, expected_targets, + project_id, subnet_id) + + observed_targets = self._sync_route_members_impl( + m_get_drv_lbaas, m_get_drv_project, m_get_drv_subnets, + subnet_id, project_id, endpoints, state, spec) + + self.assertEqual(sorted(expected_targets.items()), observed_targets) + + def test_on_deleted_no_ingress_controller(self): + endpoints = mock.sentinel.endpoints + m_handler = mock.Mock(spec=h_ing_lbaas.IngressLoadBalancerHandler) + m_handler._l7_router = None + h_ing_lbaas.IngressLoadBalancerHandler.on_deleted(m_handler, endpoints) + + m_handler._get_lbaas_route_state.assert_not_called() + m_handler._remove_unused_route_members.assert_not_called() + + def test_on_deleted(self): + endpoints = mock.sentinel.endpoints + project_id = uuidutils.generate_uuid() + subnet_id = uuidutils.generate_uuid() + + m_handler = mock.Mock(spec=h_ing_lbaas.IngressLoadBalancerHandler) + m_handler._l7_router = t_lbaas.FakeLBaaSDriver().ensure_loadbalancer( + name='L7_Router', + project_id=project_id, + subnet_id=subnet_id, + ip='1.2.3.4', + security_groups_ids=None, + service_type='ClusterIP') + + m_handler._get_lbaas_route_state.return_value = ( + obj_lbaas.LBaaSRouteState()) + m_handler._remove_unused_route_members.return_value = True + + h_ing_lbaas.IngressLoadBalancerHandler.on_deleted(m_handler, endpoints) + + m_handler._get_lbaas_route_state.assert_called_once() + m_handler._remove_unused_route_members.assert_called_once() diff --git a/kuryr_kubernetes/tests/unit/controller/handlers/test_lbaas.py b/kuryr_kubernetes/tests/unit/controller/handlers/test_lbaas.py index 802a15bc4..8c0296cc1 100644 --- a/kuryr_kubernetes/tests/unit/controller/handlers/test_lbaas.py +++ b/kuryr_kubernetes/tests/unit/controller/handlers/test_lbaas.py @@ -428,7 +428,10 @@ class FakeLBaaSDriver(drv_base.LBaaSDriver): def ensure_pool_attached_to_lb(self, loadbalancer, namespace, svc_name, protocol): - pass + return obj_lbaas.LBaaSPool(id=uuidutils.generate_uuid(), + loadbalancer_id=loadbalancer.id, + project_id=loadbalancer.project_id, + protocol=protocol) def get_pool_by_name(self, pool_name, project_id): pass diff --git a/kuryr_kubernetes/tests/unit/platform/ocp/__init__.py b/kuryr_kubernetes/tests/unit/platform/ocp/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/kuryr_kubernetes/tests/unit/platform/ocp/controller/handlers/__init__.py b/kuryr_kubernetes/tests/unit/platform/ocp/controller/handlers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/kuryr_kubernetes/tests/unit/platform/ocp/controller/handlers/test_route.py b/kuryr_kubernetes/tests/unit/platform/ocp/controller/handlers/test_route.py new file mode 100644 index 000000000..3e0c60f7e --- /dev/null +++ b/kuryr_kubernetes/tests/unit/platform/ocp/controller/handlers/test_route.py @@ -0,0 +1,403 @@ +# Copyright (c) 2017 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.controller.drivers import l7_router as d_l7_rtr +from kuryr_kubernetes.objects import lbaas as obj_lbaas +from kuryr_kubernetes.objects import route as obj_route +from kuryr_kubernetes.platform.ocp.controller.handlers import route as h_route +from kuryr_kubernetes.tests import base as test_base +import mock + + +class TestOcpRouteHandler(test_base.TestCase): + + @mock.patch('kuryr_kubernetes.controller.drivers.base' + '.L7RouterDriver.get_instance') + def test_init(self, m_get_drv_l7_router): + m_get_drv_l7_router.return_value = mock.sentinel.drv_l7_router + + handler = h_route.OcpRouteHandler() + self.assertEqual(mock.sentinel.drv_l7_router, handler._drv_l7_router) + self.assertIsNone(handler._l7_router) + self.assertIsNone(handler._l7_router_listeners) + + def test_on_present(self): + route_event = mock.sentinel.route_event + route_spec = mock.sentinel.route_spec + route_state = mock.sentinel.route_state + route_spec.to_service = mock.sentinel.to_service + m_handler = mock.Mock(spec=h_route.OcpRouteHandler) + m_handler._get_route_spec.return_value = route_spec + m_handler._should_ignore.return_value = False + m_handler._get_route_state.return_value = route_state + m_handler._l7_router = obj_lbaas.LBaaSLoadBalancer( + id='00EE9E11-91C2-41CF-8FD4-7970579E5C4C', + project_id='TEST_PROJECT') + + h_route.OcpRouteHandler.on_present(m_handler, route_event) + m_handler._sync_router_pool.assert_called_once_with( + route_event, route_spec, route_state) + m_handler._sync_l7_policy.assert_called_once_with( + route_event, route_spec, route_state) + m_handler._sync_host_l7_rule.assert_called_once_with( + route_event, route_spec, route_state) + m_handler._sync_path_l7_rule.assert_called_once_with( + route_event, route_spec, route_state) + m_handler._set_route_state.assert_called_once_with( + route_event, route_state) + m_handler._set_route_spec.assert_called_once_with( + route_event, route_spec) + m_handler._send_route_notification_to_ep.assert_called_once_with( + route_event, route_spec.to_service) + + def test_on_present_no_change(self): + route_event = mock.sentinel.route_event + route_spec = mock.sentinel.route_spec + + m_handler = mock.Mock(spec=h_route.OcpRouteHandler) + m_handler._get_route_spec.return_value = route_spec + m_handler._should_ignore.return_value = True + m_handler._l7_router = obj_lbaas.LBaaSLoadBalancer( + id='00EE9E11-91C2-41CF-8FD4-7970579E5C4C', + project_id='TEST_PROJECT') + + h_route.OcpRouteHandler.on_present(m_handler, route_event) + m_handler._get_route_spec.assert_called_once_with( + route_event) + m_handler._sync_router_pool.assert_not_called() + m_handler._sync_l7_policy.assert_not_called() + m_handler._sync_host_l7_rule.assert_not_called() + m_handler._sync_path_l7_rule.assert_not_called() + m_handler._set_route_state.assert_not_called() + m_handler._set_route_spec.assert_not_called() + m_handler._send_route_notification_to_ep.assert_not_called() + + def test_get_endpoints_link_by_route(self): + m_handler = mock.Mock(spec=h_route.OcpRouteHandler) + route_link = '/oapi/v1/namespaces/default/routes/my_route' + ep_name = 'my_endpoint' + expected_ep_link = '/api/v1/namespaces/default/endpoints/my_endpoint' + ret_ep_path = h_route.OcpRouteHandler._get_endpoints_link_by_route( + m_handler, route_link, ep_name) + + self.assertEqual(expected_ep_link, ret_ep_path) + + def test_get_endpoints_link_by_route_error(self): + m_handler = mock.Mock(spec=h_route.OcpRouteHandler) + route_link = '/oapi/v1/namespaces/default/routes/my_route' + ep_name = 'wrong_endpoint' + expected_ep_link = '/api/v1/namespaces/default/endpoints/my_endpoint' + ret_ep_path = h_route.OcpRouteHandler._get_endpoints_link_by_route( + m_handler, route_link, ep_name) + + self.assertNotEqual(expected_ep_link, ret_ep_path) + + def test_should_ignore_l7_router_not_exist(self): + m_handler = mock.Mock(spec=h_route.OcpRouteHandler) + m_handler._l7_router = None + route = {'spec': { + 'host': 'www.test.com', 'path': 'mypath', + 'to': {'name': 'target_service'}}} + + route_spec = obj_route.RouteSpec( + host='www.test.com', + path='mypath', + to_service='target_service') + expected_result = True + + ret_value = h_route.OcpRouteHandler._should_ignore( + m_handler, route, route_spec) + + self.assertEqual(ret_value, expected_result) + + def test_should_ignore_l7_router_exist_no_change(self): + m_handler = mock.Mock(spec=h_route.OcpRouteHandler) + m_handler._l7_router = obj_lbaas.LBaaSLoadBalancer( + id='00EE9E11-91C2-41CF-8FD4-7970579E5C4C', + project_id='TEST_PROJECT') + route = {'spec': { + 'host': 'www.test.com', 'path': 'mypath', + 'to': {'name': 'target_service'}}} + + route_spec = obj_route.RouteSpec( + host='www.test.com', + path='mypath', + to_service='target_service') + expected_result = True + ret_value = h_route.OcpRouteHandler._should_ignore( + m_handler, route, route_spec) + self.assertEqual(ret_value, expected_result) + + def test_should_ignore_l7_router_exist_with_changes(self): + m_handler = mock.Mock(spec=h_route.OcpRouteHandler) + m_handler._l7_router = obj_lbaas.LBaaSLoadBalancer( + id='00EE9E11-91C2-41CF-8FD4-7970579E5C4C', + project_id='TEST_PROJECT') + route = {'spec': { + 'host': 'www.test.com', 'path': 'mypath', + 'to': {'name': 'target_service'}}} + route_spec = obj_route.RouteSpec( + host='www.test.com1', + path='mypath', + to_service='target_service') + expected_result = False + ret_value = h_route.OcpRouteHandler._should_ignore( + m_handler, route, route_spec) + self.assertEqual(ret_value, expected_result) + + def test_sync_router_pool_empty_pool(self): + m_handler = mock.Mock(spec=h_route.OcpRouteHandler) + m_handler._l7_router = obj_lbaas.LBaaSLoadBalancer( + id='00EE9E11-91C2-41CF-8FD4-7970579E5C4C', + project_id='TEST_PROJECT') + m_handler._drv_l7_router = mock.Mock( + spec=d_l7_rtr.LBaaSv2L7RouterDriver) + m_handler._drv_l7_router.ensure_pool.return_value = None + + route = {'metadata': {'namespace': 'namespace'}, + 'spec': {'host': 'www.test.com', 'path': 'mypath', + 'to': {'name': 'target_service'}}} + route_spec = obj_route.RouteSpec( + host='www.test.com1', + path='mypath', + to_service='target_service') + + route_state = obj_route.RouteState() + + h_route.OcpRouteHandler._sync_router_pool( + m_handler, route, route_spec, route_state) + self.assertIsNone(route_state.router_pool) + + def test_sync_router_pool_valid_pool(self): + m_handler = mock.Mock(spec=h_route.OcpRouteHandler) + m_handler._l7_router = obj_lbaas.LBaaSLoadBalancer( + id='00EE9E11-91C2-41CF-8FD4-7970579E5C4C', + project_id='TEST_PROJECT') + m_handler._drv_l7_router = mock.Mock( + spec=d_l7_rtr.LBaaSv2L7RouterDriver) + ret_pool = obj_lbaas.LBaaSPool( + name='TEST_NAME', project_id='TEST_PROJECT', protocol='TCP', + listener_id='A57B7771-6050-4CA8-A63C-443493EC98AB', + loadbalancer_id='00EE9E11-91C2-41CF-8FD4-7970579E5C4C') + + m_handler._drv_l7_router.ensure_pool.return_value = ret_pool + route = {'metadata': {'namespace': 'namespace'}, + 'spec': {'host': 'www.test.com', 'path': 'mypath', + 'to': {'name': 'target_service'}}} + route_spec = obj_route.RouteSpec( + host='www.test.com1', + path='mypath', + to_service='target_service') + + route_state = obj_route.RouteState() + + h_route.OcpRouteHandler._sync_router_pool( + m_handler, route, route_spec, route_state) + self.assertEqual(route_state.router_pool, ret_pool) + + def test_sync_l7_policy(self): + m_handler = mock.Mock(spec=h_route.OcpRouteHandler) + m_handler._l7_router = obj_lbaas.LBaaSLoadBalancer( + id='00EE9E11-91C2-41CF-8FD4-7970579E5C4C', + project_id='TEST_PROJECT') + m_handler._drv_l7_router = mock.Mock( + spec=d_l7_rtr.LBaaSv2L7RouterDriver) + listener = obj_lbaas.LBaaSListener( + id='123443545', + name='TEST_NAME', project_id='TEST_PROJECT', protocol='TCP', + port=80, loadbalancer_id='00EE9E11-91C2-41CF-8FD4-7970579E5C4C') + m_handler._l7_router_listeners = {'80': listener} + l7_policy = obj_route.RouteL7Policy( + id='00EE9E11-91C2-41CF-8FD4-7970579E5C44', name='myname', + listener_id='00EE9E11-91C2-41CF-8FD4-7970579E5C45', + redirect_pool_id='00EE9E11-91C2-41CF-8FD4-7970579E5C46', + project_id='00EE9E11-91C2-41CF-8FD4-7970579E5C46') + + route_state = obj_route.RouteState() + m_handler._drv_l7_router.ensure_l7_policy.return_value = l7_policy + + route = {'metadata': {'namespace': 'namespace', 'name': 'name'}, + 'spec': {'host': 'www.test.com', 'path': 'mypath', + 'to': {'name': 'target_service'}}} + route_spec = obj_route.RouteSpec( + host='www.test.com1', + path='mypath', + to_service='target_service') + + h_route.OcpRouteHandler._sync_l7_policy( + m_handler, route, route_spec, route_state) + self.assertEqual(route_state.l7_policy, l7_policy) + + def test_sync_host_l7_rule_already_exist(self): + m_handler = mock.Mock(spec=h_route.OcpRouteHandler) + m_handler._l7_router = obj_lbaas.LBaaSLoadBalancer( + id='00EE9E11-91C2-41CF-8FD4-7970579E5C4C', + project_id='TEST_PROJECT') + m_handler._drv_l7_router = mock.Mock( + spec=d_l7_rtr.LBaaSv2L7RouterDriver) + h_l7_rule = obj_route.RouteL7Rule( + id='00EE9E11-91C2-41CF-8FD4-7970579E5C44', + compare_type='EQUAL_TO', + l7policy_id='00EE9E11-91C2-41CF-8FD4-7970579E5C45', + type='HOST', + value='www.example.com') + + route_state = obj_route.RouteState(h_l7_rule=h_l7_rule) + route = {'metadata': {'namespace': 'namespace', 'name': 'name'}, + 'spec': {'host': 'www.test.com', 'path': 'mypath', + 'to': {'name': 'target_service'}}} + + route_spec = obj_route.RouteSpec( + host='www.test.com', + path='mypath', + to_service='target_service') + + h_route.OcpRouteHandler._sync_host_l7_rule( + m_handler, route, route_spec, route_state) + self.assertEqual(route_state.h_l7_rule, h_l7_rule) + + def test_sync_host_l7_rule_new_host(self): + m_handler = mock.Mock(spec=h_route.OcpRouteHandler) + m_handler._l7_router = obj_lbaas.LBaaSLoadBalancer( + id='00EE9E11-91C2-41CF-8FD4-7970579E5C4C', + project_id='TEST_PROJECT') + m_handler._drv_l7_router = mock.Mock( + spec=d_l7_rtr.LBaaSv2L7RouterDriver) + h_l7_rule = obj_route.RouteL7Rule( + id='00EE9E11-91C2-41CF-8FD4-7970579E5C44', + compare_type='EQUAL_TO', + l7policy_id='00EE9E11-91C2-41CF-8FD4-7970579E5C45', + type='HOST', + value='www.example.com') + + route_state = obj_route.RouteState(h_l7_rule=h_l7_rule) + + route = {'metadata': {'namespace': 'namespace', 'name': 'name'}, + 'spec': {'host': 'new.www.test.com', 'path': 'mypath', + 'to': {'name': 'target_service'}}} + route_spec = obj_route.RouteSpec( + host='www.test.com', + path='mypath', + to_service='target_service') + + m_handler._drv_l7_router.ensure_l7_rule.return_value = h_l7_rule + h_route.OcpRouteHandler._sync_host_l7_rule( + m_handler, route, route_spec, route_state) + self.assertEqual(route_state.h_l7_rule.value, route['spec']['host']) + + def test_sync_path_l7_rule(self): + m_handler = mock.Mock(spec=h_route.OcpRouteHandler) + m_handler._l7_router = obj_lbaas.LBaaSLoadBalancer( + id='00EE9E11-91C2-41CF-8FD4-7970579E5C4C', + project_id='TEST_PROJECT') + m_handler._drv_l7_router = mock.Mock( + spec=d_l7_rtr.LBaaSv2L7RouterDriver) + + old_p_l7_rule = obj_route.RouteL7Rule( + id='00EE9E11-91C2-41CF-8FD4-7970579E5C44', + compare_type='EQUAL_TO', + l7policy_id='00EE9E11-91C2-41CF-8FD4-7970579E5C45', + type='PATH', + value='/nice_path/') + + route_state = obj_route.RouteState(p_l7_rule=old_p_l7_rule) + + route = {'metadata': {'namespace': 'namespace', 'name': 'name'}, + 'spec': {'host': 'new.www.test.com', 'path': 'mypath', + 'to': {'name': 'target_service'}}} + + route_spec = obj_route.RouteSpec( + host='www.test.com', + path='mypath', + to_service='target_service') + + ret_p_l7_rule = obj_route.RouteL7Rule( + id='55559E11-91C2-41CF-8FD4-7970579E5C44', + compare_type='EQUAL_TO', + l7policy_id='55559E11-91C2-41CF-8FD4-7970579E5C45', + type='PATH', + value='/nice_path/') + + m_handler._drv_l7_router.ensure_l7_rule.return_value = ret_p_l7_rule + h_route.OcpRouteHandler._sync_path_l7_rule( + m_handler, route, route_spec, route_state) + self.assertEqual(route_state.p_l7_rule, old_p_l7_rule) + + def test_sync_path_l7_rule_route_spec_path_is_none(self): + m_handler = mock.Mock(spec=h_route.OcpRouteHandler) + m_handler._l7_router = obj_lbaas.LBaaSLoadBalancer( + id='00EE9E11-91C2-41CF-8FD4-7970579E5C4C', + project_id='TEST_PROJECT') + m_handler._drv_l7_router = mock.Mock( + spec=d_l7_rtr.LBaaSv2L7RouterDriver) + + old_p_l7_rule = obj_route.RouteL7Rule( + id='00EE9E11-91C2-41CF-8FD4-7970579E5C44', + compare_type='EQUAL_TO', + l7policy_id='00EE9E11-91C2-41CF-8FD4-7970579E5C45', + type='PATH', + value='/nice_path/') + + route_state = obj_route.RouteState(p_l7_rule=old_p_l7_rule) + + route = {'metadata': {'namespace': 'namespace', 'name': 'name'}, + 'spec': {'host': 'new.www.test.com', 'path': 'mypath', + 'to': {'name': 'target_service'}}} + route_spec = obj_route.RouteSpec( + host='www.test.com', + path=None, + to_service='target_service') + + ret_p_l7_rule = obj_route.RouteL7Rule( + id='55559E11-91C2-41CF-8FD4-7970579E5C44', + compare_type='EQUAL_TO', + l7policy_id='55559E11-91C2-41CF-8FD4-7970579E5C45', + type='PATH', + value='/nice_path/') + + m_handler._drv_l7_router.ensure_l7_rule.return_value = ret_p_l7_rule + h_route.OcpRouteHandler._sync_path_l7_rule( + m_handler, route, route_spec, route_state) + self.assertEqual(route_state.p_l7_rule, ret_p_l7_rule) + + def test_sync_path_l7_rule_route_spec_not_sync(self): + m_handler = mock.Mock(spec=h_route.OcpRouteHandler) + m_handler._l7_router = obj_lbaas.LBaaSLoadBalancer( + id='00EE9E11-91C2-41CF-8FD4-7970579E5C4C', + project_id='TEST_PROJECT') + m_handler._drv_l7_router = mock.Mock( + spec=d_l7_rtr.LBaaSv2L7RouterDriver) + + old_p_l7_rule = obj_route.RouteL7Rule( + id='00EE9E11-91C2-41CF-8FD4-7970579E5C44', + compare_type='EQUAL_TO', + l7policy_id='00EE9E11-91C2-41CF-8FD4-7970579E5C45', + type='PATH', + value='/nice_path/') + + route_state = obj_route.RouteState(p_l7_rule=old_p_l7_rule) + + route = {'metadata': {'namespace': 'namespace', 'name': 'name'}, + 'spec': {'host': 'new.www.test.com', 'path': 'new_path', + 'to': {'name': 'target_service'}}} + + route_spec = obj_route.RouteSpec( + host='www.test.com', + path='path', + to_service='target_service') + m_handler._drv_l7_router.update_l7_rule.return_value = None + + h_route.OcpRouteHandler._sync_path_l7_rule( + m_handler, route, route_spec, route_state) + self.assertEqual(route_state.p_l7_rule.value, route['spec']['path']) diff --git a/releasenotes/notes/bp-openshift-router-support-5f28108b39a2826f.yaml b/releasenotes/notes/bp-openshift-router-support-5f28108b39a2826f.yaml new file mode 100644 index 000000000..651273b98 --- /dev/null +++ b/releasenotes/notes/bp-openshift-router-support-5f28108b39a2826f.yaml @@ -0,0 +1,15 @@ +--- +features: + - | + An OpenShift route is a way to expose a service by giving it an + externally-reachable hostname like www.example.com. + A defined route and the endpoints identified by its service can be + consumed by a router to provide named connectivity that allows external + clients to reach your applications. + Each route consists of a route name , target service details. + To enable it the following handlers should be added : + + .. code-block:: ini + + [kubernetes] + enabled_handlers=vif,lb,lbaasspec,ingresslb,ocproute diff --git a/setup.cfg b/setup.cfg index 5c284d7ab..31c8a9d24 100644 --- a/setup.cfg +++ b/setup.cfg @@ -81,6 +81,8 @@ kuryr_kubernetes.controller.handlers = lbaasspec = kuryr_kubernetes.controller.handlers.lbaas:LBaaSSpecHandler lb = kuryr_kubernetes.controller.handlers.lbaas:LoadBalancerHandler namespace = kuryr_kubernetes.controller.handlers.namespace:NamespaceHandler + ingresslb = kuryr_kubernetes.controller.handlers.ingress_lbaas:IngressLoadBalancerHandler + ocproute = kuryr_kubernetes.platform.ocp.controller.handlers.route:OcpRouteHandler test_handler = kuryr_kubernetes.tests.unit.controller.handlers.test_fake_handler:TestHandler [files]