mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-07 18:14:10 +00:00
Compare commits
34 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b186bdbce3 | ||
![]() |
36fe6c6f66 | ||
![]() |
8bf559db52 | ||
![]() |
750085f627 | ||
![]() |
2dc2c99b4a | ||
![]() |
e703555888 | ||
![]() |
7e102f0511 | ||
![]() |
facde96425 | ||
![]() |
608c746a59 | ||
![]() |
a8c834410f | ||
![]() |
bda14b487a | ||
![]() |
fd5cf8c360 | ||
![]() |
03758e5b46 | ||
![]() |
e540d143bb | ||
![]() |
b2c5ad40c5 | ||
![]() |
edfdf672d8 | ||
![]() |
39f19aef49 | ||
![]() |
8813bb63d4 | ||
![]() |
7c18d6fe14 | ||
![]() |
d1fe17d3cb | ||
![]() |
b8965c2017 | ||
![]() |
733d7bc158 | ||
![]() |
88f31c29bb | ||
![]() |
3caf3cfda8 | ||
![]() |
d076c55cca | ||
![]() |
3e185022c8 | ||
![]() |
857ee2885f | ||
![]() |
cd8dd56213 | ||
![]() |
f06902aa8f | ||
![]() |
bb109c6f75 | ||
![]() |
e525ec7b5b | ||
![]() |
356b98e19f | ||
![]() |
8c803e7a53 | ||
![]() |
2e21a6f4e0 |
24
.github/workflows/release.yml
vendored
24
.github/workflows/release.yml
vendored
@@ -150,7 +150,7 @@ jobs:
|
||||
|
||||
- name: Get list of jupyterhub-onbuild tags
|
||||
id: onbuildtags
|
||||
uses: jupyterhub/action-major-minor-tag-calculator@v1
|
||||
uses: jupyterhub/action-major-minor-tag-calculator@v2
|
||||
with:
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
prefix: "${{ env.REGISTRY }}jupyterhub/jupyterhub-onbuild:"
|
||||
@@ -171,7 +171,7 @@ jobs:
|
||||
|
||||
- name: Get list of jupyterhub-demo tags
|
||||
id: demotags
|
||||
uses: jupyterhub/action-major-minor-tag-calculator@v1
|
||||
uses: jupyterhub/action-major-minor-tag-calculator@v2
|
||||
with:
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
prefix: "${{ env.REGISTRY }}jupyterhub/jupyterhub-demo:"
|
||||
@@ -190,3 +190,23 @@ jobs:
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ join(fromJson(steps.demotags.outputs.tags)) }}
|
||||
|
||||
# jupyterhub/singleuser
|
||||
- name: Get list of jupyterhub/singleuser tags
|
||||
id: singleusertags
|
||||
uses: jupyterhub/action-major-minor-tag-calculator@v2
|
||||
with:
|
||||
githubToken: ${{ secrets.GITHUB_TOKEN }}
|
||||
prefix: "${{ env.REGISTRY }}jupyterhub/singleuser:"
|
||||
defaultTag: "${{ env.REGISTRY }}jupyterhub/singleuser:noref"
|
||||
branchRegex: ^\w[\w-.]*$
|
||||
|
||||
- name: Build and push jupyterhub/singleuser
|
||||
uses: docker/build-push-action@e1b7f96249f2e4c8e4ac1519b9608c0d48944a1f # associated tag: v2.4.0
|
||||
with:
|
||||
build-args: |
|
||||
JUPYTERHUB_VERSION=${{ github.ref_type == 'tag' && github.ref_name || format('git:{0}', github.sha) }}
|
||||
context: singleuser
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ join(fromJson(steps.singleusertags.outputs.tags)) }}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.29.0
|
||||
rev: v2.29.1
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args:
|
||||
@@ -10,7 +10,7 @@ repos:
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 21.9b0
|
||||
rev: 21.11b1
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
BIN
docs/source/images/binder-404.png
Normal file
BIN
docs/source/images/binder-404.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 160 KiB |
BIN
docs/source/images/binderhub-form.png
Normal file
BIN
docs/source/images/binderhub-form.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 138 KiB |
BIN
docs/source/images/chp-404.png
Normal file
BIN
docs/source/images/chp-404.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
BIN
docs/source/images/server-not-running.png
Normal file
BIN
docs/source/images/server-not-running.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
@@ -114,7 +114,9 @@ class ScopeTableGenerator:
|
||||
if doc_description:
|
||||
description = doc_description
|
||||
scope_dict[scope] = description
|
||||
content['securityDefinitions']['oauth2']['scopes'] = scope_dict
|
||||
content['components']['securitySchemes']['oauth2']['flows'][
|
||||
'authorizationCode'
|
||||
]['scopes'] = scope_dict
|
||||
|
||||
with open(filename, 'w') as f:
|
||||
yaml.dump(content, f)
|
||||
|
128
docs/source/reference/api-only.md
Normal file
128
docs/source/reference/api-only.md
Normal file
@@ -0,0 +1,128 @@
|
||||
(api-only)=
|
||||
|
||||
# Deploying JupyterHub in "API only mode"
|
||||
|
||||
As a service for deploying and managing Jupyter servers for users, JupyterHub
|
||||
exposes this functionality _primarily_ via a [REST API](rest).
|
||||
For convenience, JupyterHub also ships with a _basic_ web UI built using that REST API.
|
||||
The basic web UI enables users to click a button to quickly start and stop their servers,
|
||||
and it lets admins perform some basic user and server management tasks.
|
||||
|
||||
The REST API has always provided additional functionality beyond what is available in the basic web UI.
|
||||
Similarly, we avoid implementing UI functionality that is also not available via the API.
|
||||
With JupyterHub 2.0, the basic web UI will **always** be composed using the REST API.
|
||||
In other words, no UI pages should rely on information not available via the REST API.
|
||||
Previously, some admin UI functionality could only be achieved via admin pages,
|
||||
such as paginated requests.
|
||||
|
||||
## Limited UI customization via templates
|
||||
|
||||
The JupyterHub UI is customizable via extensible HTML [templates](templates),
|
||||
but this has some limited scope to what can be customized.
|
||||
Adding some content and messages to existing pages is well supported,
|
||||
but changing the page flow and what pages are available are beyond the scope of what is customizable.
|
||||
|
||||
## Rich UI customization with REST API based apps
|
||||
|
||||
Increasingly, JupyterHub is used purely as an API for managing Jupyter servers
|
||||
for other Jupyter-based applications that might want to present a different user experience.
|
||||
If you want a fully customized user experience,
|
||||
you can now disable the Hub UI and use your own pages together with the JupyterHub REST API
|
||||
to build your own web application to serve your users,
|
||||
relying on the Hub only as an API for managing users and servers.
|
||||
|
||||
One example of such an application is [BinderHub][], which powers https://mybinder.org,
|
||||
and motivates many of these changes.
|
||||
|
||||
BinderHub is distinct from a traditional JupyterHub deployment
|
||||
because it uses temporary users created for each launch.
|
||||
Instead of presenting a login page,
|
||||
users are presented with a form to specify what environment they would like to launch:
|
||||
|
||||

|
||||
|
||||
When a launch is requested:
|
||||
|
||||
1. an image is built, if necessary
|
||||
2. a temporary user is created,
|
||||
3. a server is launched for that user, and
|
||||
4. when running, users are redirected to an already running server with an auth token in the URL
|
||||
5. after the session is over, the user is deleted
|
||||
|
||||
This means that a lot of JupyterHub's UI flow doesn't make sense:
|
||||
|
||||
- there is no way for users to login
|
||||
- the human user doesn't map onto a JupyterHub `User` in a meaningful way
|
||||
- when a server isn't running, there isn't a 'restart your server' action available because the user has been deleted
|
||||
- users do not have any access to any Hub functionality, so presenting pages for those features would be confusing
|
||||
|
||||
BinderHub is one of the motivating use cases for JupyterHub supporting being used _only_ via its API.
|
||||
We'll use BinderHub here as an example of various configuration options.
|
||||
|
||||
[binderhub]: https://binderhub.readthedocs.io
|
||||
|
||||
## Disabling Hub UI
|
||||
|
||||
`c.JupyterHub.hub_routespec` is a configuration option to specify which URL prefix should be routed to the Hub.
|
||||
The default is `/` which means that the Hub will receive all requests not already specified to be routed somewhere else.
|
||||
|
||||
There are three values that are most logical for `hub_routespec`:
|
||||
|
||||
- `/` - this is the default, and used in most deployments.
|
||||
It is also the only option prior to JupyterHub 1.4.
|
||||
- `/hub/` - this serves only Hub pages, both UI and API
|
||||
- `/hub/api` - this serves _only the Hub API_, so all Hub UI is disabled,
|
||||
aside from the OAuth confirmation page, if used.
|
||||
|
||||
If you choose a hub routespec other than `/`,
|
||||
the main JupyterHub feature you will lose is the automatic handling of requests for `/user/:username`
|
||||
when the requested server is not running.
|
||||
|
||||
JupyterHub's handling of this request shows this page,
|
||||
telling you that the server is not running,
|
||||
with a button to launch it again:
|
||||
|
||||

|
||||
|
||||
If you set `hub_routespec` to something other than `/`,
|
||||
it is likely that you also want to register another destination for `/` to handle requests to not-running servers.
|
||||
If you don't, you will see a default 404 page from the proxy:
|
||||
|
||||

|
||||
|
||||
For mybinder.org, the default "start my server" page doesn't make sense,
|
||||
because when a server is gone, there is no restart action.
|
||||
Instead, we provide hints about how to get back to a link to start a _new_ server:
|
||||
|
||||

|
||||
|
||||
To achieve this, mybinder.org registers a route for `/` that goes to a custom endpoint
|
||||
that runs nginx and only serves this static HTML error page.
|
||||
This is set with
|
||||
|
||||
```python
|
||||
c.Proxy.extra_routes = {
|
||||
"/": "http://custom-404-entpoint/",
|
||||
}
|
||||
```
|
||||
|
||||
You may want to use an alternate behavior, such as redirecting to a landing page,
|
||||
or taking some other action based on the requested page.
|
||||
|
||||
If you use `c.JupyterHub.hub_routespec = "/hub/"`,
|
||||
then all the Hub pages will be available,
|
||||
and only this default-page-404 issue will come up.
|
||||
|
||||
If you use `c.JupyterHub.hub_routespec = "/hub/api/"`,
|
||||
then only the Hub _API_ will be available,
|
||||
and all UI will be up to you.
|
||||
mybinder.org takes this last option,
|
||||
because none of the Hub UI pages really make sense.
|
||||
Binder users don't have any reason to know or care that JupyterHub happens
|
||||
to be an implementation detail of how their environment is managed.
|
||||
Seeing Hub error pages and messages in that situation is more likely to be confusing than helpful.
|
||||
|
||||
:::{versionadded} 1.4
|
||||
|
||||
`c.JupyterHub.hub_routespec` and `c.Proxy.extra_routes` are new in JupyterHub 1.4.
|
||||
:::
|
@@ -21,6 +21,7 @@ what happens under-the-hood when you deploy and configure your JupyterHub.
|
||||
monitoring
|
||||
database
|
||||
templates
|
||||
api-only
|
||||
../events/index
|
||||
config-user-env
|
||||
config-examples
|
||||
|
@@ -5,7 +5,7 @@ Below is an interactive view of JupyterHub's OpenAPI specification.
|
||||
<!-- client-rendered openapi UI copied from FastAPI -->
|
||||
|
||||
<link type="text/css" rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui-bundle.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@4.1/swagger-ui-bundle.js"></script>
|
||||
<!-- `SwaggerUIBundle` is now available on the page -->
|
||||
|
||||
<!-- render the ui here -->
|
||||
|
@@ -1,3 +1,5 @@
|
||||
(rest-api)=
|
||||
|
||||
# Using JupyterHub's REST API
|
||||
|
||||
This section will give you information on:
|
||||
|
@@ -2,7 +2,7 @@
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
# version_info updated by running `tbump`
|
||||
version_info = (2, 0, 0, "rc2", "")
|
||||
version_info = (2, 0, 0, "rc5", "")
|
||||
|
||||
# pep 440 version: no dot before beta/rc, but before .dev
|
||||
# 0.1.0rc1
|
||||
|
@@ -31,6 +31,9 @@ class APIHandler(BaseHandler):
|
||||
- methods for REST API models
|
||||
"""
|
||||
|
||||
# accept token-based authentication for API requests
|
||||
_accept_token_auth = True
|
||||
|
||||
@property
|
||||
def content_security_policy(self):
|
||||
return '; '.join([super().content_security_policy, "default-src 'none'"])
|
||||
@@ -210,6 +213,7 @@ class APIHandler(BaseHandler):
|
||||
'last_activity': isoformat(token.last_activity),
|
||||
'expires_at': isoformat(token.expires_at),
|
||||
'note': token.note,
|
||||
'session_id': token.session_id,
|
||||
'oauth_client': token.oauth_client.description
|
||||
or token.oauth_client.identifier,
|
||||
}
|
||||
|
@@ -58,6 +58,14 @@ class SelfAPIHandler(APIHandler):
|
||||
|
||||
model = get_model(user)
|
||||
|
||||
# add session_id associated with token
|
||||
# added in 2.0
|
||||
token = self.get_token()
|
||||
if token:
|
||||
model["session_id"] = token.session_id
|
||||
else:
|
||||
model["session_id"] = None
|
||||
|
||||
# add scopes to identify model,
|
||||
# but not the scopes we added to ensure we could read our own model
|
||||
model["scopes"] = sorted(self.expanded_scopes.difference(_added_scopes))
|
||||
|
@@ -71,6 +71,12 @@ SESSION_COOKIE_NAME = 'jupyterhub-session-id'
|
||||
class BaseHandler(RequestHandler):
|
||||
"""Base Handler class with access to common methods and properties."""
|
||||
|
||||
# by default, only accept cookie-based authentication
|
||||
# The APIHandler base class enables token auth
|
||||
# versionadded: 2.0
|
||||
_accept_cookie_auth = True
|
||||
_accept_token_auth = False
|
||||
|
||||
async def prepare(self):
|
||||
"""Identify the user during the prepare stage of each request
|
||||
|
||||
@@ -340,6 +346,7 @@ class BaseHandler(RequestHandler):
|
||||
auth_info['auth_state'] = await user.get_auth_state()
|
||||
return await self.auth_to_user(auth_info, user)
|
||||
|
||||
@functools.lru_cache()
|
||||
def get_token(self):
|
||||
"""get token from authorization header"""
|
||||
token = self.get_auth_token()
|
||||
@@ -410,9 +417,11 @@ class BaseHandler(RequestHandler):
|
||||
async def get_current_user(self):
|
||||
"""get current username"""
|
||||
if not hasattr(self, '_jupyterhub_user'):
|
||||
user = None
|
||||
try:
|
||||
user = self.get_current_user_token()
|
||||
if user is None:
|
||||
if self._accept_token_auth:
|
||||
user = self.get_current_user_token()
|
||||
if user is None and self._accept_cookie_auth:
|
||||
user = self.get_current_user_cookie()
|
||||
if user and isinstance(user, User):
|
||||
user = await self.refresh_auth(user)
|
||||
|
@@ -295,7 +295,7 @@ def get_scopes_for(orm_object):
|
||||
)
|
||||
|
||||
if isinstance(orm_object, orm.APIToken):
|
||||
app_log.warning(f"Authenticated with token {orm_object}")
|
||||
app_log.debug(f"Authenticated with token {orm_object}")
|
||||
owner = orm_object.user or orm_object.service
|
||||
token_scopes = roles.expand_roles_to_scopes(orm_object)
|
||||
if orm_object.client_id != "jupyterhub":
|
||||
|
@@ -1023,8 +1023,8 @@ class HubAuthenticated:
|
||||
self._hub_auth_user_cache = None
|
||||
raise
|
||||
|
||||
# store tokens passed via url or header in a cookie for future requests
|
||||
url_token = self.hub_auth.get_token(self)
|
||||
# store ?token=... tokens passed via url in a cookie for future requests
|
||||
url_token = self.get_argument('token', '')
|
||||
if (
|
||||
user_model
|
||||
and url_token
|
||||
|
@@ -18,6 +18,7 @@ import sys
|
||||
import warnings
|
||||
from datetime import datetime
|
||||
from datetime import timezone
|
||||
from importlib import import_module
|
||||
from textwrap import dedent
|
||||
from urllib.parse import urlparse
|
||||
|
||||
@@ -606,10 +607,34 @@ class SingleUserNotebookAppMixin(Configurable):
|
||||
t = self.hub_activity_interval * (1 + 0.2 * (random.random() - 0.5))
|
||||
await asyncio.sleep(t)
|
||||
|
||||
def _log_app_versions(self):
|
||||
"""Log application versions at startup
|
||||
|
||||
Logs versions of jupyterhub and singleuser-server base versions (jupyterlab, jupyter_server, notebook)
|
||||
"""
|
||||
self.log.info(f"Starting jupyterhub single-user server version {__version__}")
|
||||
|
||||
# don't log these package versions
|
||||
seen = {"jupyterhub", "traitlets", "jupyter_core", "builtins"}
|
||||
|
||||
for cls in self.__class__.mro():
|
||||
module_name = cls.__module__.partition(".")[0]
|
||||
if module_name not in seen:
|
||||
seen.add(module_name)
|
||||
try:
|
||||
mod = import_module(module_name)
|
||||
mod_version = getattr(mod, "__version__")
|
||||
except Exception:
|
||||
mod_version = ""
|
||||
self.log.info(
|
||||
f"Extending {cls.__module__}.{cls.__name__} from {module_name} {mod_version}"
|
||||
)
|
||||
|
||||
def initialize(self, argv=None):
|
||||
# disable trash by default
|
||||
# this can be re-enabled by config
|
||||
self.config.FileContentsManager.delete_to_trash = False
|
||||
self._log_app_versions()
|
||||
return super().initialize(argv)
|
||||
|
||||
def start(self):
|
||||
@@ -715,6 +740,18 @@ class SingleUserNotebookAppMixin(Configurable):
|
||||
orig_loader = env.loader
|
||||
env.loader = ChoiceLoader([FunctionLoader(get_page), orig_loader])
|
||||
|
||||
def load_server_extensions(self):
|
||||
# Loading LabApp sets $JUPYTERHUB_API_TOKEN on load, which is incorrect
|
||||
r = super().load_server_extensions()
|
||||
# clear the token in PageConfig at this step
|
||||
# so that cookie auth is used
|
||||
# FIXME: in the future,
|
||||
# it would probably make sense to set page_config.token to the token
|
||||
# from the current request.
|
||||
if 'page_config_data' in self.web_app.settings:
|
||||
self.web_app.settings['page_config_data']['token'] = ''
|
||||
return r
|
||||
|
||||
|
||||
def detect_base_package(App):
|
||||
"""Detect the base package for an App class
|
||||
|
@@ -578,6 +578,41 @@ async def test_login_page(app, url, params, redirected_url, form_action):
|
||||
assert action.endswith(form_action)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"url, token_in",
|
||||
[
|
||||
("/home", "url"),
|
||||
("/home", "header"),
|
||||
("/login", "url"),
|
||||
("/login", "header"),
|
||||
],
|
||||
)
|
||||
async def test_page_with_token(app, user, url, token_in):
|
||||
cookies = await app.login_user(user.name)
|
||||
token = user.new_api_token()
|
||||
if token_in == "url":
|
||||
url = url_concat(url, {"token": token})
|
||||
headers = None
|
||||
elif token_in == "header":
|
||||
headers = {
|
||||
"Authorization": f"token {token}",
|
||||
}
|
||||
|
||||
# request a page with ?token= in URL shouldn't be allowed
|
||||
r = await get_page(
|
||||
url,
|
||||
app,
|
||||
headers=headers,
|
||||
allow_redirects=False,
|
||||
)
|
||||
if "/hub/login" in r.url:
|
||||
assert r.status_code == 200
|
||||
else:
|
||||
assert r.status_code == 302
|
||||
assert r.headers["location"].partition("?")[0].endswith("/hub/login")
|
||||
assert not r.cookies
|
||||
|
||||
|
||||
async def test_login_fail(app):
|
||||
name = 'wash'
|
||||
base_url = public_url(app)
|
||||
|
@@ -1332,3 +1332,19 @@ async def test_token_keep_roles_on_restart():
|
||||
for token in user.api_tokens:
|
||||
hub.db.delete(token)
|
||||
hub.db.commit()
|
||||
|
||||
|
||||
async def test_login_default_role(app, username):
|
||||
cookies = await app.login_user(username)
|
||||
user = app.users[username]
|
||||
# assert login new user gets 'user' role
|
||||
assert [role.name for role in user.roles] == ["user"]
|
||||
|
||||
# clear roles, keep user
|
||||
user.roles = []
|
||||
app.db.commit()
|
||||
|
||||
# login *again*; user exists, shouldn't trigger change in roles
|
||||
cookies = await app.login_user(username)
|
||||
user = app.users[username]
|
||||
assert user.roles == []
|
||||
|
@@ -11,7 +11,7 @@ target_version = [
|
||||
github_url = "https://github.com/jupyterhub/jupyterhub"
|
||||
|
||||
[tool.tbump.version]
|
||||
current = "2.0.0rc2"
|
||||
current = "2.0.0rc5"
|
||||
|
||||
# Example of a semver regexp.
|
||||
# Make sure this matches current_version before
|
||||
|
5
setup.py
5
setup.py
@@ -46,10 +46,9 @@ def get_data_files():
|
||||
"""Get data files in share/jupyter"""
|
||||
|
||||
data_files = []
|
||||
ntrim = len(here + os.path.sep)
|
||||
|
||||
for (d, dirs, filenames) in os.walk(share_jupyterhub):
|
||||
data_files.append((d[ntrim:], [pjoin(d, f) for f in filenames]))
|
||||
rel_d = os.path.relpath(d, here)
|
||||
data_files.append((rel_d, [os.path.join(rel_d, f) for f in filenames]))
|
||||
return data_files
|
||||
|
||||
|
||||
|
@@ -6,7 +6,6 @@ FROM $BASE_IMAGE
|
||||
MAINTAINER Project Jupyter <jupyter@googlegroups.com>
|
||||
|
||||
ADD install_jupyterhub /tmp/install_jupyterhub
|
||||
ARG JUPYTERHUB_VERSION=main
|
||||
# install pinned jupyterhub and ensure jupyterlab is installed
|
||||
RUN python3 /tmp/install_jupyterhub && \
|
||||
python3 -m pip install jupyterlab
|
||||
ARG JUPYTERHUB_VERSION=git:HEAD
|
||||
# install pinned jupyterhub
|
||||
RUN python3 /tmp/install_jupyterhub
|
||||
|
@@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -ex
|
||||
|
||||
docker build --build-arg JUPYTERHUB_VERSION=$DOCKER_TAG -t $DOCKER_REPO:$DOCKER_TAG .
|
@@ -1,21 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -ex
|
||||
|
||||
function get_hub_version() {
|
||||
rm -f hub_version
|
||||
V=$1
|
||||
docker run --rm -v $PWD:/version -u $(id -u) -i $DOCKER_REPO:$DOCKER_TAG sh -c 'jupyterhub --version > /version/hub_version'
|
||||
hub_xyz=$(cat hub_version)
|
||||
split=( ${hub_xyz//./ } )
|
||||
hub_xy="${split[0]}.${split[1]}"
|
||||
# add .dev on hub_xy so it's 1.0.dev
|
||||
if [[ ! -z "${split[3]:-}" ]]; then
|
||||
hub_xy="${hub_xy}.${split[3]}"
|
||||
fi
|
||||
}
|
||||
# tag e.g. 0.9 with main
|
||||
get_hub_version
|
||||
docker tag $DOCKER_REPO:$DOCKER_TAG $DOCKER_REPO:$hub_xy
|
||||
docker push $DOCKER_REPO:$hub_xy
|
||||
docker tag $DOCKER_REPO:$DOCKER_TAG $DOCKER_REPO:$hub_xyz
|
||||
docker push $DOCKER_REPO:$hub_xyz
|
@@ -3,19 +3,22 @@ import os
|
||||
from subprocess import check_call
|
||||
import sys
|
||||
|
||||
V = os.environ['JUPYTERHUB_VERSION']
|
||||
version = os.environ['JUPYTERHUB_VERSION']
|
||||
|
||||
pip_install = [
|
||||
sys.executable, '-m', 'pip', 'install', '--no-cache', '--upgrade',
|
||||
'--upgrade-strategy', 'only-if-needed',
|
||||
sys.executable,
|
||||
'-m',
|
||||
'pip',
|
||||
'install',
|
||||
'--no-cache',
|
||||
'--upgrade',
|
||||
'--upgrade-strategy',
|
||||
'only-if-needed',
|
||||
]
|
||||
if V in {'main', 'HEAD'}:
|
||||
req = 'https://github.com/jupyterhub/jupyterhub/archive/HEAD.tar.gz'
|
||||
if version.startswith("git:"):
|
||||
ref = version.partition(":")[-1]
|
||||
req = f"https://github.com/jupyterhub/jupyterhub/archive/{ref}.tar.gz"
|
||||
else:
|
||||
version_info = [ int(part) for part in V.split('.') ]
|
||||
version_info[-1] += 1
|
||||
upper_bound = '.'.join(map(str, version_info))
|
||||
vs = '>=%s,<%s' % (V, upper_bound)
|
||||
req = 'jupyterhub%s' % vs
|
||||
req = f"jupyterhub=={version}"
|
||||
|
||||
check_call(pip_install + [req])
|
||||
|
Reference in New Issue
Block a user