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

View File

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

View File

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

View File

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

View File

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