mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-10 11:33:01 +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
|
||||
|
||||
- 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
|
||||
- Add statsd support, via `c.JupyterHub.statsd_{host,port,prefix}`
|
||||
- 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.
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import warnings
|
||||
@@ -48,6 +49,17 @@ from ..utils import make_ssl_context
|
||||
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
|
||||
|
||||
|
||||
@@ -268,6 +280,10 @@ class SingleUserNotebookAppMixin(Configurable):
|
||||
def _user_changed(self, change):
|
||||
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_prefix = Unicode('/hub/').tag(config=True)
|
||||
@@ -350,7 +366,26 @@ class SingleUserNotebookAppMixin(Configurable):
|
||||
""",
|
||||
).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):
|
||||
value = os.path.expanduser(proposal['value'])
|
||||
# Strip any trailing slashes
|
||||
@@ -366,6 +401,13 @@ class SingleUserNotebookAppMixin(Configurable):
|
||||
raise TraitError("No such notebook dir: %r" % 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')
|
||||
def _log_datefmt_default(self):
|
||||
"""Exclude date from default date format"""
|
||||
|
@@ -797,8 +797,27 @@ class Spawner(LoggingConfigurable):
|
||||
'activity',
|
||||
)
|
||||
env['JUPYTERHUB_BASE_URL'] = self.hub.base_url[:-4]
|
||||
|
||||
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
|
||||
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.
|
||||
# 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_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
|
||||
# one parameter - the current spawner instance - and the return value
|
||||
# will be assigned to the environment variable. This will be called at
|
||||
@@ -829,7 +862,6 @@ class Spawner(LoggingConfigurable):
|
||||
env[key] = value(self)
|
||||
else:
|
||||
env[key] = value
|
||||
|
||||
return env
|
||||
|
||||
async def get_url(self):
|
||||
@@ -996,24 +1028,16 @@ class Spawner(LoggingConfigurable):
|
||||
"""Return the arguments to be passed after self.cmd
|
||||
|
||||
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 = []
|
||||
|
||||
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
|
||||
return self.args
|
||||
|
||||
def run_pre_spawn_hook(self):
|
||||
"""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.
|
||||
"""
|
||||
|
||||
@default('ip')
|
||||
def _default_ip(self):
|
||||
"""Listen on localhost by default for local processes"""
|
||||
return '127.0.0.1'
|
||||
|
||||
interrupt_timeout = Integer(
|
||||
10,
|
||||
help="""
|
||||
|
@@ -11,10 +11,10 @@ Handlers and their purpose include:
|
||||
- ArgsHandler: allowing retrieval of `sys.argv`.
|
||||
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from tornado import httpserver
|
||||
from tornado import ioloop
|
||||
@@ -36,7 +36,8 @@ class ArgsHandler(web.RequestHandler):
|
||||
self.write(json.dumps(sys.argv))
|
||||
|
||||
|
||||
def main(args):
|
||||
def main():
|
||||
url = urlparse(os.environ["JUPYTERHUB_SERVICE_URL"])
|
||||
options.logging = 'debug'
|
||||
log.enable_pretty_logging()
|
||||
app = web.Application(
|
||||
@@ -50,10 +51,11 @@ def main(args):
|
||||
|
||||
if key and cert and ca:
|
||||
ssl_context = make_ssl_context(key, cert, cafile=ca, check_hostname=False)
|
||||
assert url.scheme == "https"
|
||||
|
||||
server = httpserver.HTTPServer(app, ssl_options=ssl_context)
|
||||
log.app_log.info("Starting mock singleuser server at 127.0.0.1:%s", args.port)
|
||||
server.listen(args.port, '127.0.0.1')
|
||||
log.app_log.info(f"Starting mock singleuser server at {url.hostname}:{url.port}")
|
||||
server.listen(url.port, url.hostname)
|
||||
try:
|
||||
ioloop.IOLoop.instance().start()
|
||||
except KeyboardInterrupt:
|
||||
@@ -61,7 +63,4 @@ def main(args):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--port', type=int)
|
||||
args, extra = parser.parse_known_args()
|
||||
main(args)
|
||||
main()
|
||||
|
@@ -565,10 +565,17 @@ async def test_spawn(app):
|
||||
r = await async_requests.get(ujoin(url, 'args'), **kwargs)
|
||||
assert r.status_code == 200
|
||||
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)
|
||||
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
|
||||
if app.subdomain_host:
|
||||
assert env['JUPYTERHUB_HOST'] == app.subdomain_host
|
||||
|
Reference in New Issue
Block a user