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:
Min RK
2021-05-04 12:30:39 +02:00
parent e75dd1b79c
commit f28b92a99e
5 changed files with 107 additions and 30 deletions

View File

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

View File

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

View File

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

View File

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

View File

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