Repository: jupyterhub/jupyterhub

Issue:
Have the hub explicitly add a default route to the proxy that points back to the hub
The hub should add a default route to the proxy that points `/` back to itself. This makes the lives of proxy implementations much easier.


Discussion:

Resolving patch:
diff --git a/jupyterhub/app.py b/jupyterhub/app.py
--- a/jupyterhub/app.py
+++ b/jupyterhub/app.py
@@ -1176,7 +1176,7 @@ def init_proxy(self):
             base_url=self.base_url,
         )
         self.proxy = self.proxy_class(
-            db=self.db,
+            db_factory=lambda: self.db,
             public_url=public_url,
             parent=self,
             app=self,
@@ -1433,6 +1433,7 @@ def start(self):
                 self.exit(1)
         else:
             self.log.info("Not starting proxy")
+        yield self.proxy.add_hub_route(self.hub)
 
         # start the service(s)
         for service_name, service in self._service_map.items():
diff --git a/jupyterhub/proxy.py b/jupyterhub/proxy.py
--- a/jupyterhub/proxy.py
+++ b/jupyterhub/proxy.py
@@ -67,7 +67,11 @@ class Proxy(LoggingConfigurable):
       of fetching a single route.
     """
 
-    db = Any()
+    db_factory = Any()
+    @property
+    def db(self):
+        return self.db_factory()
+
     app = Any()
     hub = Any()
     public_url = Unicode()
@@ -107,6 +111,10 @@ def validate_routespec(self, routespec):
         - Checks host value vs host-based routing.
         - Ensures trailing slash on path.
         """
+        if routespec == '/':
+            # / is the default route.
+            # don't check host-based routing
+            return routespec
         # check host routing
         host_route = not routespec.startswith('/')
         if host_route and not self.host_routing:
@@ -281,6 +289,11 @@ def check_routes(self, user_dict, service_dict, routes=None):
         user_routes = {r['data']['user'] for r in routes.values() if 'user' in r['data']}
         futures = []
         db = self.db
+
+        if '/' not in routes:
+            self.log.warning("Adding missing default route")
+            self.add_hub_route(self.app.hub)
+
         for orm_user in db.query(User):
             user = user_dict[orm_user]
             if user.running:
@@ -315,9 +328,15 @@ def check_routes(self, user_dict, service_dict, routes=None):
         for f in futures:
             yield f
 
+    def add_hub_route(self, hub):
+        """Add the default route for the Hub"""
+        self.log.info("Adding default route for Hub: / => %s", hub.host)
+        return self.add_route('/', self.hub.host)
+
     @gen.coroutine
     def restore_routes(self):
         self.log.info("Setting up routes on new proxy")
+        yield self.add_hub_route(self.app.hub)
         yield self.add_all_users(self.app.users)
         yield self.add_all_services(self.app.services)
         self.log.info("New proxy back up and good to go")
@@ -379,7 +398,6 @@ def start(self):
             '--port', str(public_server.port),
             '--api-ip', api_server.ip,
             '--api-port', str(api_server.port),
-            '--default-target', self.hub.host,
             '--error-target', url_path_join(self.hub.url, 'error'),
         ]
         if self.app.subdomain_host:
@@ -431,7 +449,6 @@ def _check_process():
                 else:
                     break
             yield server.wait_up(1)
-        time.sleep(1)
         _check_process()
         self.log.debug("Proxy started and appears to be up")
         pc = PeriodicCallback(self.check_running, 1e3 * self.check_running_interval)
@@ -464,7 +481,7 @@ def _routespec_to_chp_path(self, routespec):
         path = self.validate_routespec(routespec)
         # CHP always wants to start with /
         if not path.startswith('/'):
-            path = path + '/'
+            path = '/' + path
         # BUG: CHP doesn't seem to like trailing slashes on some endpoints (DELETE)
         if path != '/' and path.endswith('/'):
             path = path.rstrip('/')
@@ -507,6 +524,7 @@ def api_request(self, path, method='GET', body=None, client=None):
     def add_route(self, routespec, target, data=None):
         body = data or {}
         body['target'] = target
+        body['jupyterhub'] = True
         path = self._routespec_to_chp_path(routespec)
         return self.api_request(path,
                                 method='POST',
@@ -520,6 +538,7 @@ def delete_route(self, routespec):
     def _reformat_routespec(self, routespec, chp_data):
         """Reformat CHP data format to JupyterHub's proxy API."""
         target = chp_data.pop('target')
+        chp_data.pop('jupyterhub')
         return {
             'routespec': routespec,
             'target': target,
@@ -534,6 +553,10 @@ def get_all_routes(self, client=None):
         all_routes = {}
         for chp_path, chp_data in chp_routes.items():
             routespec = self._routespec_from_chp_path(chp_path)
+            if 'jupyterhub' not in chp_data:
+                # exclude routes not associated with JupyterHub
+                self.log.debug("Omitting non-jupyterhub route %r", routespec)
+                continue
             all_routes[routespec] = self._reformat_routespec(
                 routespec, chp_data)
         return all_routes


Test patch:
diff --git a/jupyterhub/tests/mocking.py b/jupyterhub/tests/mocking.py
--- a/jupyterhub/tests/mocking.py
+++ b/jupyterhub/tests/mocking.py
@@ -5,6 +5,7 @@
 from tempfile import NamedTemporaryFile
 import threading
 from unittest import mock
+from urllib.parse import urlparse
 
 import requests
 
@@ -136,7 +137,15 @@ def _subdomain_host_default(self):
     @default('ip')
     def _ip_default(self):
         return '127.0.0.1'
-    
+
+    @default('port')
+    def _port_default(self):
+        if self.subdomain_host:
+            port = urlparse(self.subdomain_host).port
+            if port:
+                return port
+        return random_port()
+
     @default('authenticator_class')
     def _authenticator_class_default(self):
         return MockPAMAuthenticator
diff --git a/jupyterhub/tests/test_proxy.py b/jupyterhub/tests/test_proxy.py
--- a/jupyterhub/tests/test_proxy.py
+++ b/jupyterhub/tests/test_proxy.py
@@ -43,7 +43,7 @@ def fin():
         '--port', str(app.port),
         '--api-ip', proxy_ip,
         '--api-port', str(proxy_port),
-        '--default-target', 'http://%s:%i' % (app.hub_ip, app.hub_port),
+        '--log-level=debug',
     ]
     if app.subdomain_host:
         cmd.append('--host-routing')
@@ -90,7 +90,7 @@ def wait_for_proxy():
 
     routes = io_loop.run_sync(app.proxy.get_all_routes)
 
-    assert list(routes.keys()) == ['/']
+    assert list(routes.keys()) == []
     
     # poke the server to update the proxy
     r = api_request(app, 'proxy', method='post')