mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-15 22:13:00 +00:00
support services subdomain
- all services are on the 'services' domain, share the same cookie
This commit is contained in:
@@ -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
|
||||
|
@@ -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(
|
||||
|
@@ -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:
|
||||
|
||||
|
@@ -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()
|
||||
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user