support services subdomain

- all services are on the 'services' domain, share the same cookie
This commit is contained in:
Min RK
2016-09-02 13:21:46 +02:00
parent 9a002c2445
commit c3111b04bb
5 changed files with 78 additions and 53 deletions

View File

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

View File

@@ -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.
@@ -202,24 +179,33 @@ class Service(LoggingConfigurable):
"""
).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(

View File

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

View File

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

View File

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