mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-11 20:13:02 +00:00
remove all CLI args from default Spawner implementation
use only env variables, which are safer to ignore and easier to handle in multiple ways
This commit is contained in:
@@ -899,7 +899,7 @@ Bugfixes on 0.6:
|
|||||||
|
|
||||||
### [0.6.0] - 2016-04-25
|
### [0.6.0] - 2016-04-25
|
||||||
|
|
||||||
- JupyterHub has moved to a new `jupyterhub` namespace on GitHub and Docker. What was `juptyer/jupyterhub` is now `jupyterhub/jupyterhub`, etc.
|
- JupyterHub has moved to a new `jupyterhub` namespace on GitHub and Docker. What was `jupyter/jupyterhub` is now `jupyterhub/jupyterhub`, etc.
|
||||||
- `jupyterhub/jupyterhub` image on DockerHub no longer loads the jupyterhub_config.py in an ONBUILD step. A new `jupyterhub/jupyterhub-onbuild` image does this
|
- `jupyterhub/jupyterhub` image on DockerHub no longer loads the jupyterhub_config.py in an ONBUILD step. A new `jupyterhub/jupyterhub-onbuild` image does this
|
||||||
- Add statsd support, via `c.JupyterHub.statsd_{host,port,prefix}`
|
- Add statsd support, via `c.JupyterHub.statsd_{host,port,prefix}`
|
||||||
- Update to traitlets 4.1 `@default`, `@observe` APIs for traits
|
- Update to traitlets 4.1 `@default`, `@observe` APIs for traits
|
||||||
|
@@ -10,6 +10,7 @@ with JupyterHub authentication mixins enabled.
|
|||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import warnings
|
import warnings
|
||||||
@@ -48,6 +49,17 @@ from ..utils import make_ssl_context
|
|||||||
from ..utils import url_path_join
|
from ..utils import url_path_join
|
||||||
|
|
||||||
|
|
||||||
|
def _bool_env(key):
|
||||||
|
"""Cast an environment variable to bool
|
||||||
|
|
||||||
|
0, empty, or unset is False; All other values are True.
|
||||||
|
"""
|
||||||
|
if os.environ.get(key, "") in {"", "0"}:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
# Authenticate requests with the Hub
|
# Authenticate requests with the Hub
|
||||||
|
|
||||||
|
|
||||||
@@ -268,6 +280,10 @@ class SingleUserNotebookAppMixin(Configurable):
|
|||||||
def _user_changed(self, change):
|
def _user_changed(self, change):
|
||||||
self.log.name = change.new
|
self.log.name = change.new
|
||||||
|
|
||||||
|
@default("default_url")
|
||||||
|
def _default_url(self):
|
||||||
|
return os.environ.get("JUPYTERHUB_DEFAULT_URL", "/tree/")
|
||||||
|
|
||||||
hub_host = Unicode().tag(config=True)
|
hub_host = Unicode().tag(config=True)
|
||||||
|
|
||||||
hub_prefix = Unicode('/hub/').tag(config=True)
|
hub_prefix = Unicode('/hub/').tag(config=True)
|
||||||
@@ -350,7 +366,26 @@ class SingleUserNotebookAppMixin(Configurable):
|
|||||||
""",
|
""",
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
@validate('notebook_dir')
|
@default("disable_user_config")
|
||||||
|
def _default_disable_user_config(self):
|
||||||
|
return _bool_env("JUPYTERHUB_DISABLE_USER_CONFIG")
|
||||||
|
|
||||||
|
@default("root_dir")
|
||||||
|
def _default_root_dir(self):
|
||||||
|
if os.environ.get("JUPYTERHUB_ROOT_DIR"):
|
||||||
|
proposal = {"value": os.environ["JUPYTERHUB_ROOT_DIR"]}
|
||||||
|
# explicitly call validator, not called on default values
|
||||||
|
return self._notebook_dir_validate(proposal)
|
||||||
|
else:
|
||||||
|
return os.getcwd()
|
||||||
|
|
||||||
|
# notebook_dir is used by the classic notebook server
|
||||||
|
# root_dir is the future in jupyter server
|
||||||
|
@default("notebook_dir")
|
||||||
|
def _default_notebook_dir(self):
|
||||||
|
return self._default_root_dir()
|
||||||
|
|
||||||
|
@validate("notebook_dir", "root_dir")
|
||||||
def _notebook_dir_validate(self, proposal):
|
def _notebook_dir_validate(self, proposal):
|
||||||
value = os.path.expanduser(proposal['value'])
|
value = os.path.expanduser(proposal['value'])
|
||||||
# Strip any trailing slashes
|
# Strip any trailing slashes
|
||||||
@@ -366,6 +401,13 @@ class SingleUserNotebookAppMixin(Configurable):
|
|||||||
raise TraitError("No such notebook dir: %r" % value)
|
raise TraitError("No such notebook dir: %r" % value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
@default('log_level')
|
||||||
|
def _log_level_defaul(self):
|
||||||
|
if _bool_env("JUPYTERHUB_DEBUG"):
|
||||||
|
return logging.DEBUG
|
||||||
|
else:
|
||||||
|
return logging.INFO
|
||||||
|
|
||||||
@default('log_datefmt')
|
@default('log_datefmt')
|
||||||
def _log_datefmt_default(self):
|
def _log_datefmt_default(self):
|
||||||
"""Exclude date from default date format"""
|
"""Exclude date from default date format"""
|
||||||
|
@@ -797,8 +797,27 @@ class Spawner(LoggingConfigurable):
|
|||||||
'activity',
|
'activity',
|
||||||
)
|
)
|
||||||
env['JUPYTERHUB_BASE_URL'] = self.hub.base_url[:-4]
|
env['JUPYTERHUB_BASE_URL'] = self.hub.base_url[:-4]
|
||||||
|
|
||||||
if self.server:
|
if self.server:
|
||||||
|
base_url = self.server.base_url
|
||||||
|
if self.ip or self.port:
|
||||||
|
self.server.ip = self.ip
|
||||||
|
self.server.port = self.port
|
||||||
env['JUPYTERHUB_SERVICE_PREFIX'] = self.server.base_url
|
env['JUPYTERHUB_SERVICE_PREFIX'] = self.server.base_url
|
||||||
|
else:
|
||||||
|
# this should only occur in mock/testing schenarios
|
||||||
|
base_url = '/'
|
||||||
|
|
||||||
|
if self.ip or self.port:
|
||||||
|
# specify JUPYTERHUB_SERVICE_URL *if* ip or port is specified
|
||||||
|
# TODO: set this always?
|
||||||
|
# Prior to 2.0, leaving 'ip' or 'port' unset meant letting the subprocess default be used
|
||||||
|
# setting via a URL means we cannot specify explicit ip or port without specifying *both*
|
||||||
|
# this results in a changed default behavior of specifying only report
|
||||||
|
# specifying ip='' *explicitly*, which is the same as all interfaces, instead of localhost
|
||||||
|
s = 's' if self.internal_ssl else ''
|
||||||
|
bind_url = f"http{s}://{self.ip}:{self.port}{base_url}"
|
||||||
|
env["JUPYTERHUB_SERVICE_URL"] = bind_url
|
||||||
|
|
||||||
# Put in limit and guarantee info if they exist.
|
# Put in limit and guarantee info if they exist.
|
||||||
# Note that this is for use by the humans / notebook extensions in the
|
# Note that this is for use by the humans / notebook extensions in the
|
||||||
@@ -818,6 +837,20 @@ class Spawner(LoggingConfigurable):
|
|||||||
env['JUPYTERHUB_SSL_CERTFILE'] = self.cert_paths['certfile']
|
env['JUPYTERHUB_SSL_CERTFILE'] = self.cert_paths['certfile']
|
||||||
env['JUPYTERHUB_SSL_CLIENT_CA'] = self.cert_paths['cafile']
|
env['JUPYTERHUB_SSL_CLIENT_CA'] = self.cert_paths['cafile']
|
||||||
|
|
||||||
|
if self.notebook_dir:
|
||||||
|
notebook_dir = self.format_string(self.notebook_dir)
|
||||||
|
env["JUPYTERHUB_ROOT_DIR"] = notebook_dir
|
||||||
|
|
||||||
|
if self.default_url:
|
||||||
|
default_url = self.format_string(self.default_url)
|
||||||
|
env["JUPYTERHUB_DEFAULT_URL"] = default_url
|
||||||
|
|
||||||
|
if self.debug:
|
||||||
|
env["JUPYTERHUB_DEBUG"] = "1"
|
||||||
|
|
||||||
|
if self.disable_user_config:
|
||||||
|
env["JUPYTERHUB_DISABLE_USER_CONFIG"] = "1"
|
||||||
|
|
||||||
# env overrides from config. If the value is a callable, it will be called with
|
# env overrides from config. If the value is a callable, it will be called with
|
||||||
# one parameter - the current spawner instance - and the return value
|
# one parameter - the current spawner instance - and the return value
|
||||||
# will be assigned to the environment variable. This will be called at
|
# will be assigned to the environment variable. This will be called at
|
||||||
@@ -829,7 +862,6 @@ class Spawner(LoggingConfigurable):
|
|||||||
env[key] = value(self)
|
env[key] = value(self)
|
||||||
else:
|
else:
|
||||||
env[key] = value
|
env[key] = value
|
||||||
|
|
||||||
return env
|
return env
|
||||||
|
|
||||||
async def get_url(self):
|
async def get_url(self):
|
||||||
@@ -996,24 +1028,16 @@ class Spawner(LoggingConfigurable):
|
|||||||
"""Return the arguments to be passed after self.cmd
|
"""Return the arguments to be passed after self.cmd
|
||||||
|
|
||||||
Doesn't expect shell expansion to happen.
|
Doesn't expect shell expansion to happen.
|
||||||
|
|
||||||
|
.. versionchanged:: 2.0
|
||||||
|
Prior to 2.0, JupyterHub passed some options such as
|
||||||
|
ip, port, and default_url to the command-line.
|
||||||
|
JupyterHub 2.0 no longer builds any CLI args
|
||||||
|
other than `Spawner.cmd` and `Spawner.args`.
|
||||||
|
All values that come from jupyterhub itself
|
||||||
|
will be passed via environment variables.
|
||||||
"""
|
"""
|
||||||
args = []
|
return self.args
|
||||||
|
|
||||||
if self.notebook_dir:
|
|
||||||
notebook_dir = self.format_string(self.notebook_dir)
|
|
||||||
args.append('--notebook-dir=%s' % _quote_safe(notebook_dir))
|
|
||||||
if self.default_url:
|
|
||||||
default_url = self.format_string(self.default_url)
|
|
||||||
args.append(
|
|
||||||
'--SingleUserNotebookApp.default_url=%s' % _quote_safe(default_url)
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.debug:
|
|
||||||
args.append('--debug')
|
|
||||||
if self.disable_user_config:
|
|
||||||
args.append('--disable-user-config')
|
|
||||||
args.extend(self.args)
|
|
||||||
return args
|
|
||||||
|
|
||||||
def run_pre_spawn_hook(self):
|
def run_pre_spawn_hook(self):
|
||||||
"""Run the pre_spawn_hook if defined"""
|
"""Run the pre_spawn_hook if defined"""
|
||||||
@@ -1269,6 +1293,11 @@ class LocalProcessSpawner(Spawner):
|
|||||||
Note: This spawner does not implement CPU / memory guarantees and limits.
|
Note: This spawner does not implement CPU / memory guarantees and limits.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@default('ip')
|
||||||
|
def _default_ip(self):
|
||||||
|
"""Listen on localhost by default for local processes"""
|
||||||
|
return '127.0.0.1'
|
||||||
|
|
||||||
interrupt_timeout = Integer(
|
interrupt_timeout = Integer(
|
||||||
10,
|
10,
|
||||||
help="""
|
help="""
|
||||||
|
@@ -11,10 +11,10 @@ Handlers and their purpose include:
|
|||||||
- ArgsHandler: allowing retrieval of `sys.argv`.
|
- ArgsHandler: allowing retrieval of `sys.argv`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import argparse
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from tornado import httpserver
|
from tornado import httpserver
|
||||||
from tornado import ioloop
|
from tornado import ioloop
|
||||||
@@ -36,7 +36,8 @@ class ArgsHandler(web.RequestHandler):
|
|||||||
self.write(json.dumps(sys.argv))
|
self.write(json.dumps(sys.argv))
|
||||||
|
|
||||||
|
|
||||||
def main(args):
|
def main():
|
||||||
|
url = urlparse(os.environ["JUPYTERHUB_SERVICE_URL"])
|
||||||
options.logging = 'debug'
|
options.logging = 'debug'
|
||||||
log.enable_pretty_logging()
|
log.enable_pretty_logging()
|
||||||
app = web.Application(
|
app = web.Application(
|
||||||
@@ -50,10 +51,11 @@ def main(args):
|
|||||||
|
|
||||||
if key and cert and ca:
|
if key and cert and ca:
|
||||||
ssl_context = make_ssl_context(key, cert, cafile=ca, check_hostname=False)
|
ssl_context = make_ssl_context(key, cert, cafile=ca, check_hostname=False)
|
||||||
|
assert url.scheme == "https"
|
||||||
|
|
||||||
server = httpserver.HTTPServer(app, ssl_options=ssl_context)
|
server = httpserver.HTTPServer(app, ssl_options=ssl_context)
|
||||||
log.app_log.info("Starting mock singleuser server at 127.0.0.1:%s", args.port)
|
log.app_log.info(f"Starting mock singleuser server at {url.hostname}:{url.port}")
|
||||||
server.listen(args.port, '127.0.0.1')
|
server.listen(url.port, url.hostname)
|
||||||
try:
|
try:
|
||||||
ioloop.IOLoop.instance().start()
|
ioloop.IOLoop.instance().start()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
@@ -61,7 +63,4 @@ def main(args):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
parser = argparse.ArgumentParser()
|
main()
|
||||||
parser.add_argument('--port', type=int)
|
|
||||||
args, extra = parser.parse_known_args()
|
|
||||||
main(args)
|
|
||||||
|
@@ -565,10 +565,17 @@ async def test_spawn(app):
|
|||||||
r = await async_requests.get(ujoin(url, 'args'), **kwargs)
|
r = await async_requests.get(ujoin(url, 'args'), **kwargs)
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
argv = r.json()
|
argv = r.json()
|
||||||
assert '--port' in ' '.join(argv)
|
assert '--port' not in ' '.join(argv)
|
||||||
|
# we pass no CLI args anymore:
|
||||||
|
assert len(argv) == 1
|
||||||
r = await async_requests.get(ujoin(url, 'env'), **kwargs)
|
r = await async_requests.get(ujoin(url, 'env'), **kwargs)
|
||||||
env = r.json()
|
env = r.json()
|
||||||
for expected in ['JUPYTERHUB_USER', 'JUPYTERHUB_BASE_URL', 'JUPYTERHUB_API_TOKEN']:
|
for expected in [
|
||||||
|
'JUPYTERHUB_USER',
|
||||||
|
'JUPYTERHUB_BASE_URL',
|
||||||
|
'JUPYTERHUB_API_TOKEN',
|
||||||
|
'JUPYTERHUB_SERVICE_URL',
|
||||||
|
]:
|
||||||
assert expected in env
|
assert expected in env
|
||||||
if app.subdomain_host:
|
if app.subdomain_host:
|
||||||
assert env['JUPYTERHUB_HOST'] == app.subdomain_host
|
assert env['JUPYTERHUB_HOST'] == app.subdomain_host
|
||||||
|
Reference in New Issue
Block a user