diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 37dd80a8..4ad3e5b2 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -292,6 +292,15 @@ class JupyterHub(Application): # if not specified, assume https: You have to be really explicit about HTTP! self.subdomain_host = 'https://' + new + domain = Unicode( + help="domain name without proto,port" + ) + @default('domain') + def _domain_default(self): + if not self.subdomain_host: + return '' + return urlparse(self.subdomain_host).hostname + port = Integer(8000, help="The public facing port of the proxy" ).tag(config=True) @@ -960,6 +969,12 @@ class JupyterHub(Application): def init_services(self): self._service_map = {} + if self.domain: + domain = 'services.' + self.domain + parsed = urlparse(self.subdomain_host) + host = '%s://services.%s' % (parsed.scheme, parsed.netloc) + else: + domain = host = '' for spec in self.services: if 'name' not in spec: raise ValueError('service spec must have a name: %r' % spec) @@ -972,16 +987,38 @@ class JupyterHub(Application): self.db.add(orm_service) orm_service.admin = spec.get('admin', False) self.db.commit() - service = Service( - proxy=self.proxy, hub=self.hub, base_url=self.base_url, + service = Service(parent=self, + base_url=self.base_url, db=self.db, orm=orm_service, - parent=self, - hub_api_url=self.hub.api_url) + domain=domain, host=host, + hub_api_url=self.hub.api_url, + ) + traits = service.traits(input=True) for key, value in spec.items(): if key not in traits: raise AttributeError("No such service field: %s" % key) setattr(service, key, value) + + if service.url: + parsed = urlparse(service.url) + if parsed.port is not None: + port = parsed.port + elif parsed.scheme == 'http': + port = 80 + elif parsed.scheme == 'https': + port = 443 + server = service.orm.server = orm.Server( + proto=parsed.scheme, + ip=parsed.hostname, + port=port, + cookie_name='jupyterhub-services', + base_url=service.prefix, + ) + self.db.add(server) + else: + service.orm.server = None + self._service_map[name] = service if service.managed: if not service.api_token: @@ -1193,8 +1230,6 @@ class JupyterHub(Application): else: version_hash=datetime.now().strftime("%Y%m%d%H%M%S"), - subdomain_host = self.subdomain_host - domain = urlparse(subdomain_host).hostname settings = dict( log_function=log_request, config=self.config, @@ -1217,8 +1252,8 @@ class JupyterHub(Application): template_path=self.template_paths, jinja2_env=jinja_env, version_hash=version_hash, - subdomain_host=subdomain_host, - domain=domain, + subdomain_host=self.subdomain_host, + domain=self.domain, statsd=self.statsd, ) # allow configured settings to have priority diff --git a/jupyterhub/services/service.py b/jupyterhub/services/service.py index 63b6c17b..9dfbd436 100644 --- a/jupyterhub/services/service.py +++ b/jupyterhub/services/service.py @@ -148,29 +148,6 @@ class Service(LoggingConfigurable): If managed, will be passed as JUPYTERHUB_SERVICE_URL env. """ ).tag(input=True) - @observe('url') - def _url_changed(self, change): - url = change['new'] - if not url: - self.orm.server = None - else: - parsed = urlparse(url) - if parsed.port is not None: - port = parsed.port - elif parsed.scheme == 'http': - port = 80 - elif parsed.scheme == 'https': - port = 443 - server = self.orm.server = orm.Server( - proto=parsed.scheme, - ip=parsed.hostname, - port=port, - cookie_name='jupyterhub-services', - base_url=self.proxy_path, - ) - self.db.add(server) - self.db.commit() - api_token = Unicode( help="""The API token to use for the service. @@ -201,25 +178,34 @@ class Service(LoggingConfigurable): If unspecified, run the service as the same user as the Hub. """ ).tag(input=True) - + + domain = Unicode() + host = Unicode() + # handles on globals: proxy = Any() - hub = Any() + base_url = Unicode() db = Any() orm = Any() - @default('orm') - def _orm_default(self): - return self.db.query(orm.Service).filter(orm.Service.name==self.name).first() - + @property def server(self): return self.orm.server @property - def proxy_path(self): + def prefix(self): return url_path_join(self.base_url, 'services', self.name) + @property + def proxy_path(self): + if not self.server: + return '' + if self.domain: + return url_path_join('/' + self.domain, self.server.base_url) + else: + return self.server.base_url + def __repr__(self): return "<{cls}(name={name}{managed})>".format( cls=self.__class__.__name__, @@ -239,7 +225,7 @@ class Service(LoggingConfigurable): env['JUPYTERHUB_API_TOKEN'] = self.api_token env['JUPYTERHUB_API_URL'] = self.hub_api_url env['JUPYTERHUB_BASE_URL'] = self.base_url - env['JUPYTERHUB_SERVICE_PATH'] = self.proxy_path + env['JUPYTERHUB_SERVICE_PATH'] = self.server.base_url env['JUPYTERHUB_SERVICE_URL'] = self.url self.spawner = _ServiceSpawner( diff --git a/jupyterhub/tests/mocking.py b/jupyterhub/tests/mocking.py index 9f39a2f2..5ffa962c 100644 --- a/jupyterhub/tests/mocking.py +++ b/jupyterhub/tests/mocking.py @@ -209,9 +209,16 @@ def public_host(app): return app.proxy.public_server.host -def public_url(app): +def public_url(app, user_or_service=None): """Return the full, public base URL (including prefix) of the given JupyterHub instance.""" - return public_host(app) + app.proxy.public_server.base_url + if user_or_service: + if app.subdomain_host: + host = user_or_service.host + else: + host = public_host(app) + return host + user_or_service.server.base_url + else: + return public_host(app) + app.proxy.public_server.base_url def user_url(user, app): @@ -223,11 +230,7 @@ def user_url(user, app): Returns: url (str): The public URL for user. """ - if app.subdomain_host: - host = user.host - else: - host = public_host(app) - return host + user.server.base_url + return public_url(app, user) # single-user-server mocking: diff --git a/jupyterhub/tests/test_services.py b/jupyterhub/tests/test_services.py index 84f67b20..73df830a 100644 --- a/jupyterhub/tests/test_services.py +++ b/jupyterhub/tests/test_services.py @@ -14,11 +14,13 @@ except ImportError: from urllib.parse import unquote import pytest +import requests from tornado import gen from tornado.ioloop import IOLoop import jupyterhub.services.service +from .mocking import public_url from .test_pages import get_page from ..utils import url_path_join, wait_for_http_server @@ -97,16 +99,14 @@ def test_managed_service(app, mockservice): def test_proxy_service(app, mockservice, io_loop): name = mockservice.name routes = io_loop.run_sync(app.proxy.get_routes) - assert unquote(mockservice.proxy_path) in routes - io_loop.run_sync(mockservice.server.wait_up) + url = public_url(app, mockservice) + '/foo' + r = requests.get(url, allow_redirects=False) path = '/services/{}/foo'.format(name) - r = get_page(path, app, hub=False, allow_redirects=False) r.raise_for_status() assert r.status_code == 200 assert r.text.endswith(path) -@pytest.mark.now def test_external_service(app, io_loop): name = 'external' with external_service(app, name=name) as env: @@ -125,9 +125,10 @@ def test_external_service(app, io_loop): evt.set() app.io_loop.add_callback(add_services) assert evt.wait(10) + service = app._service_map[name] + url = public_url(app, service) + '/api/users' path = '/services/{}/api/users'.format(name) - r = get_page(path, app, hub=False, allow_redirects=False) - print(r.headers, r.status_code) + r = requests.get(url, allow_redirects=False) r.raise_for_status() assert r.status_code == 200 resp = r.json() diff --git a/jupyterhub/user.py b/jupyterhub/user.py index 81d4cdaf..20f3aa8d 100644 --- a/jupyterhub/user.py +++ b/jupyterhub/user.py @@ -173,7 +173,7 @@ class User(HasTraits): @property def host(self): - """Get the *host* for my server (domain[:port])""" + """Get the *host* for my server (proto://domain[:port])""" # FIXME: escaped_name probably isn't escaped enough in general for a domain fragment parsed = urlparse(self.settings['subdomain_host']) h = '%s://%s.%s' % (parsed.scheme, self.escaped_name, parsed.netloc)