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
This commit is contained in:
Yossi Boaron 2018-01-24 15:26:08 +02:00 committed by Michał Dulko
parent 4ab102afa8
commit d5902e8fed
17 changed files with 1173 additions and 1 deletions

View File

@ -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"

View File

@ -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'])

View File

@ -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=[]),
}

View File

@ -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),
}

View File

View File

@ -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'

View File

@ -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'])

View File

@ -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()

View File

@ -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

View File

@ -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'])

View File

@ -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

View File

@ -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]