diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 20859ef6..3118b7b5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -80,7 +80,7 @@ jobs: steps: - name: Should we push this image to a public registry? run: | - if [ "${{ startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/master') }}" = "true" ]; then + if [ "${{ startsWith(github.ref, 'refs/tags/') || (github.ref == 'refs/heads/main') }}" = "true" ]; then # Empty => Docker Hub echo "REGISTRY=" >> $GITHUB_ENV else @@ -89,27 +89,32 @@ jobs: - uses: actions/checkout@v2 - # Setup docker to build for multiple platforms (requires qemu). - # See: - # https://github.com/docker/build-push-action/tree/v2.3.0#usage - # https://github.com/docker/build-push-action/blob/v2.3.0/docs/advanced/multi-platform.md + # Setup docker to build for multiple platforms, see: + # https://github.com/docker/build-push-action/tree/v2.4.0#usage + # https://github.com/docker/build-push-action/blob/v2.4.0/docs/advanced/multi-platform.md - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 + - name: Set up QEMU (for docker buildx) + uses: docker/setup-qemu-action@25f0500ff22e406f7191a2a8ba8cda16901ca018 # associated tag: v1.0.2 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + - name: Set up Docker Buildx (for multi-arch builds) + uses: docker/setup-buildx-action@2a4b53665e15ce7d7049afb11ff1f70ff1610609 # associated tag: v1.1.2 with: # Allows pushing to registry on localhost:5000 driver-opts: network=host - # https://github.com/docker/login-action/tree/v1.8.0#docker-hub - - name: Login to Docker Hub - uses: docker/login-action@v1 + - name: Setup push rights to Docker Hub + # This was setup by... + # 1. Creating a Docker Hub service account "jupyterhubbot" + # 2. Creating a access token for the service account specific to this + # repository: https://hub.docker.com/settings/security + # 3. Making the account part of the "bots" team, and granting that team + # permissions to push to the relevant images: + # https://hub.docker.com/orgs/jupyterhub/teams/bots/permissions + # 4. Registering the username and token as a secret for this repo: + # https://github.com/jupyterhub/jupyterhub/settings/secrets/actions if: env.REGISTRY != 'localhost:5000/' - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + run: | + docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" -p "${{ secrets.DOCKERHUB_TOKEN }}" # https://github.com/jupyterhub/action-major-minor-tag-calculator # If this is a tagged build this will return additional parent tags. @@ -127,7 +132,7 @@ jobs: defaultTag: "${{ env.REGISTRY }}jupyterhub/jupyterhub:noref" - name: Build and push jupyterhub - uses: docker/build-push-action@v2 + uses: docker/build-push-action@e1b7f96249f2e4c8e4ac1519b9608c0d48944a1f # associated tag: v2.4.0 with: context: . platforms: linux/amd64,linux/arm64 @@ -147,7 +152,7 @@ jobs: defaultTag: "${{ env.REGISTRY }}jupyterhub/jupyterhub-onbuild:noref" - name: Build and push jupyterhub-onbuild - uses: docker/build-push-action@v2 + uses: docker/build-push-action@e1b7f96249f2e4c8e4ac1519b9608c0d48944a1f # associated tag: v2.4.0 with: build-args: | BASE_IMAGE=${{ fromJson(steps.jupyterhubtags.outputs.tags)[0] }} @@ -167,7 +172,7 @@ jobs: defaultTag: "${{ env.REGISTRY }}jupyterhub/jupyterhub-demo:noref" - name: Build and push jupyterhub-demo - uses: docker/build-push-action@v2 + uses: docker/build-push-action@e1b7f96249f2e4c8e4ac1519b9608c0d48944a1f # associated tag: v2.4.0 with: build-args: | BASE_IMAGE=${{ fromJson(steps.onbuildtags.outputs.tags)[0] }} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index fb710f7c..12d406b7 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1 +1 @@ -Please refer to [Project Jupyter's Code of Conduct](https://github.com/jupyter/governance/blob/master/conduct/code_of_conduct.md). +Please refer to [Project Jupyter's Code of Conduct](https://github.com/jupyter/governance/blob/HEAD/conduct/code_of_conduct.md). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 34d3a606..abcc6e8a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,7 +3,7 @@ Welcome! As a [Jupyter](https://jupyter.org) project, you can follow the [Jupyter contributor guide](https://jupyter.readthedocs.io/en/latest/contributing/content-contributor.html). -Make sure to also follow [Project Jupyter's Code of Conduct](https://github.com/jupyter/governance/blob/master/conduct/code_of_conduct.md) +Make sure to also follow [Project Jupyter's Code of Conduct](https://github.com/jupyter/governance/blob/HEAD/conduct/code_of_conduct.md) for a friendly and welcoming collaborative environment. ## Setting up a development environment diff --git a/README.md b/README.md index 24dbd8a1..fc184552 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ [![GitHub Workflow Status - Test](https://img.shields.io/github/workflow/status/jupyterhub/jupyterhub/Test?logo=github&label=tests)](https://github.com/jupyterhub/jupyterhub/actions) [![DockerHub build status](https://img.shields.io/docker/build/jupyterhub/jupyterhub?logo=docker&label=build)](https://hub.docker.com/r/jupyterhub/jupyterhub/tags) [![CircleCI build status](https://img.shields.io/circleci/build/github/jupyterhub/jupyterhub?logo=circleci)](https://circleci.com/gh/jupyterhub/jupyterhub) -[![Test coverage of code](https://codecov.io/gh/jupyterhub/jupyterhub/branch/master/graph/badge.svg)](https://codecov.io/gh/jupyterhub/jupyterhub) +[![Test coverage of code](https://codecov.io/gh/jupyterhub/jupyterhub/branch/main/graph/badge.svg)](https://codecov.io/gh/jupyterhub/jupyterhub) [![GitHub](https://img.shields.io/badge/issue_tracking-github-blue?logo=github)](https://github.com/jupyterhub/jupyterhub/issues) [![Discourse](https://img.shields.io/badge/help_forum-discourse-blue?logo=discourse)](https://discourse.jupyter.org/c/jupyterhub) [![Gitter](https://img.shields.io/badge/social_chat-gitter-blue?logo=gitter)](https://gitter.im/jupyterhub/jupyterhub) @@ -46,7 +46,7 @@ Basic principles for operation are: servers. JupyterHub also provides a -[REST API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/jupyterhub/master/docs/rest-api.yml#/default) +[REST API](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/jupyter/jupyterhub/HEAD/docs/rest-api.yml#/default) for administration of the Hub and its users. ## Installation @@ -239,7 +239,7 @@ our JupyterHub [Gitter](https://gitter.im/jupyterhub/jupyterhub) channel. - [Reporting Issues](https://github.com/jupyterhub/jupyterhub/issues) - [JupyterHub tutorial](https://github.com/jupyterhub/jupyterhub-tutorial) - [Documentation for JupyterHub](https://jupyterhub.readthedocs.io/en/latest/) | [PDF (latest)](https://media.readthedocs.org/pdf/jupyterhub/latest/jupyterhub.pdf) | [PDF (stable)](https://media.readthedocs.org/pdf/jupyterhub/stable/jupyterhub.pdf) -- [Documentation for JupyterHub's REST API](http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/jupyterhub/master/docs/rest-api.yml#/default) +- [Documentation for JupyterHub's REST API](https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/jupyter/jupyterhub/HEAD/docs/rest-api.yml#/default) - [Documentation for Project Jupyter](http://jupyter.readthedocs.io/en/latest/index.html) | [PDF](https://media.readthedocs.org/pdf/jupyter/latest/jupyter.pdf) - [Project Jupyter website](https://jupyter.org) - [Project Jupyter community](https://jupyter.org/community) diff --git a/docs/rest-api.yml b/docs/rest-api.yml index 4af57548..6568dfe5 100644 --- a/docs/rest-api.yml +++ b/docs/rest-api.yml @@ -1,4 +1,4 @@ -# see me at: http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyterhub/jupyterhub/master/docs/rest-api.yml#/default +# see me at: https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/jupyterhub/jupyterhub/HEAD/docs/rest-api.yml#/default swagger: "2.0" info: title: JupyterHub @@ -296,7 +296,7 @@ paths: type: string - name: server_name description: | - name given to a named-server. + name given to a named-server. Note that depending on your JupyterHub infrastructure there are chracterter size limitation to `server_name`. Default spawner with K8s pod will not allow Jupyter Notebooks to be spawned with a name that contains more than 253 characters (keep in mind that the pod will be spawned with extra characters to identify the user and hub). in: path diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst index 2650b5a8..c5535af8 100644 --- a/docs/source/api/index.rst +++ b/docs/source/api/index.rst @@ -18,7 +18,7 @@ information on: - learning more about JupyterHub's API The same JupyterHub API spec, as found here, is available in an interactive form -`here (on swagger's petstore) `__. +`here (on swagger's petstore) `__. The `OpenAPI Initiative`_ (fka Swagger™) is a project used to describe and document RESTful APIs. diff --git a/docs/source/changelog.md b/docs/source/changelog.md index fb195630..daa2a6cf 100644 --- a/docs/source/changelog.md +++ b/docs/source/changelog.md @@ -24,6 +24,35 @@ This is now also configurable via `JupyterHub.oauth_token_expires_in`. The result is that it should be much less likely for auth tokens stored in cookies to expire during the lifetime of a server. +### [1.4.1] 2021-05-12 + +1.4.1 is a small bugfix release for 1.4. + +([full changelog](https://github.com/jupyterhub/jupyterhub/compare/1.4.0...1.4.1)) + +#### Enhancements made + +#### Bugs fixed + +- define Spawner.delete_forever on base Spawner [#3454](https://github.com/jupyterhub/jupyterhub/pull/3454) ([@minrk](https://github.com/minrk)) +- patch base handlers from both jupyter_server and notebook [#3437](https://github.com/jupyterhub/jupyterhub/pull/3437) ([@minrk](https://github.com/minrk)) + +#### Maintenance and upkeep improvements + +- ci: fix typo in environment variable [#3457](https://github.com/jupyterhub/jupyterhub/pull/3457) ([@consideRatio](https://github.com/consideRatio)) +- avoid re-using asyncio.Locks across event loops [#3456](https://github.com/jupyterhub/jupyterhub/pull/3456) ([@minrk](https://github.com/minrk)) +- ci: github workflow security, pin action to sha etc [#3436](https://github.com/jupyterhub/jupyterhub/pull/3436) ([@consideRatio](https://github.com/consideRatio)) + +#### Documentation improvements + +- Fix documentation [#3452](https://github.com/jupyterhub/jupyterhub/pull/3452) ([@davidbrochart](https://github.com/davidbrochart)) + +#### Contributors to this release + +([GitHub contributors page for this release](https://github.com/jupyterhub/jupyterhub/graphs/contributors?from=2021-04-19&to=2021-05-12&type=c)) + +[@0mar](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3A0mar+updated%3A2021-04-19..2021-05-12&type=Issues) | [@betatim](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Abetatim+updated%3A2021-04-19..2021-05-12&type=Issues) | [@consideRatio](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AconsideRatio+updated%3A2021-04-19..2021-05-12&type=Issues) | [@danlester](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Adanlester+updated%3A2021-04-19..2021-05-12&type=Issues) | [@davidbrochart](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Adavidbrochart+updated%3A2021-04-19..2021-05-12&type=Issues) | [@IvanaH8](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AIvanaH8+updated%3A2021-04-19..2021-05-12&type=Issues) | [@manics](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Amanics+updated%3A2021-04-19..2021-05-12&type=Issues) | [@minrk](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Aminrk+updated%3A2021-04-19..2021-05-12&type=Issues) | [@naatebarber](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Anaatebarber+updated%3A2021-04-19..2021-05-12&type=Issues) | [@OrnithOrtion](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3AOrnithOrtion+updated%3A2021-04-19..2021-05-12&type=Issues) | [@support](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Asupport+updated%3A2021-04-19..2021-05-12&type=Issues) | [@welcome](https://github.com/search?q=repo%3Ajupyterhub%2Fjupyterhub+involves%3Awelcome+updated%3A2021-04-19..2021-05-12&type=Issues) + ### 1.4.0 2021-04-19 ([full changelog](https://github.com/jupyterhub/jupyterhub/compare/1.3.0...1.4.0)) @@ -1042,7 +1071,8 @@ Fix removal of `/login` page in 0.4.0, breaking some OAuth providers. First preview release -[unreleased]: https://github.com/jupyterhub/jupyterhub/compare/1.4.0...HEAD +[unreleased]: https://github.com/jupyterhub/jupyterhub/compare/1.4.1...HEAD +[1.4.1]: https://github.com/jupyterhub/jupyterhub/compare/1.4.0...1.4.1 [1.4.0]: https://github.com/jupyterhub/jupyterhub/compare/1.3.0...1.4.0 [1.3.0]: https://github.com/jupyterhub/jupyterhub/compare/1.2.1...1.3.0 [1.2.2]: https://github.com/jupyterhub/jupyterhub/compare/1.2.1...1.2.2 diff --git a/docs/source/contributing/docs.rst b/docs/source/contributing/docs.rst index 6b6b647f..0ea17269 100644 --- a/docs/source/contributing/docs.rst +++ b/docs/source/contributing/docs.rst @@ -13,7 +13,7 @@ Building documentation locally We use `sphinx `_ to build our documentation. It takes our documentation source files (written in `markdown `_ or `reStructuredText -`_ & +`_ & stored under the ``docs/source`` directory) and converts it into various formats for people to read. To make sure the documentation you write or change renders correctly, it is good practice to test it locally. @@ -39,8 +39,8 @@ change renders correctly, it is good practice to test it locally. along with the filename / line number in which they occurred. Fix them, and re-run the ``make html`` command to re-render the documentation. -#. View the rendered documentation by opening ``build/html/index.html`` in - a web browser. +#. View the rendered documentation by opening ``build/html/index.html`` in + a web browser. .. tip:: diff --git a/docs/source/contributing/index.rst b/docs/source/contributing/index.rst index 78c02037..8eec5bd0 100644 --- a/docs/source/contributing/index.rst +++ b/docs/source/contributing/index.rst @@ -6,8 +6,8 @@ We want you to contribute to JupyterHub in ways that are most exciting & useful to you. We value documentation, testing, bug reporting & code equally, and are glad to have your contributions in whatever form you wish :) -Our `Code of Conduct `_ -(`reporting guidelines `_) +Our `Code of Conduct `_ +(`reporting guidelines `_) helps keep our community welcoming to as many people as possible. .. toctree:: diff --git a/docs/source/gallery-jhub-deployments.md b/docs/source/gallery-jhub-deployments.md index 1290c0f1..dbdd8993 100644 --- a/docs/source/gallery-jhub-deployments.md +++ b/docs/source/gallery-jhub-deployments.md @@ -30,7 +30,7 @@ Please submit pull requests to update information or to add new institutions or ### University of California Davis -- [Spinning up multiple Jupyter Notebooks on AWS for a tutorial](https://github.com/mblmicdiv/course2017/blob/master/exercises/sourmash-setup.md) +- [Spinning up multiple Jupyter Notebooks on AWS for a tutorial](https://github.com/mblmicdiv/course2017/blob/HEAD/exercises/sourmash-setup.md) Although not technically a JupyterHub deployment, this tutorial setup may be helpful to others in the Jupyter community. diff --git a/docs/source/getting-started/authenticators-users-basics.md b/docs/source/getting-started/authenticators-users-basics.md index 266ebde2..e5fc9f5c 100644 --- a/docs/source/getting-started/authenticators-users-basics.md +++ b/docs/source/getting-started/authenticators-users-basics.md @@ -22,7 +22,7 @@ Admin users of JupyterHub, `admin_users`, can add and remove users from the user `allowed_users` set. `admin_users` can take actions on other users' behalf, such as stopping and restarting their servers. -A set of initial admin users, `admin_users` can configured be as follows: +A set of initial admin users, `admin_users` can be configured as follows: ```python c.Authenticator.admin_users = {'mal', 'zoe'} @@ -32,9 +32,9 @@ Users in the admin set are automatically added to the user `allowed_users` set, if they are not already present. Each authenticator may have different ways of determining whether a user is an -administrator. By default JupyterHub use the PAMAuthenticator which provide the -`admin_groups` option and can determine administrator status base on a user -groups. For example we can let any users in the `wheel` group be admin: +administrator. By default JupyterHub uses the PAMAuthenticator which provides the +`admin_groups` option and can set administrator status based on a user +group. For example we can let any user in the `wheel` group be admin: ```python c.PAMAuthenticator.admin_groups = {'wheel'} @@ -42,9 +42,9 @@ c.PAMAuthenticator.admin_groups = {'wheel'} ## Give admin access to other users' notebook servers (`admin_access`) -Since the default `JupyterHub.admin_access` setting is False, the admins +Since the default `JupyterHub.admin_access` setting is `False`, the admins do not have permission to log in to the single user notebook servers -owned by _other users_. If `JupyterHub.admin_access` is set to True, +owned by _other users_. If `JupyterHub.admin_access` is set to `True`, then admins have permission to log in _as other users_ on their respective machines, for debugging. **As a courtesy, you should make sure your users know if admin_access is enabled.** @@ -53,8 +53,8 @@ sure your users know if admin_access is enabled.** Users can be added to and removed from the Hub via either the admin panel or the REST API. When a user is **added**, the user will be -automatically added to the allowed users set and database. Restarting the Hub -will not require manually updating the allowed users set in your config file, +automatically added to the `allowed_users` set and database. Restarting the Hub +will not require manually updating the `allowed_users` set in your config file, as the users will be loaded from the database. After starting the Hub once, it is not sufficient to **remove** a user @@ -107,8 +107,8 @@ with any provider, is also available. ## Use DummyAuthenticator for testing -The :class:`~jupyterhub.auth.DummyAuthenticator` is a simple authenticator that -allows for any username/password unless if a global password has been set. If +The `DummyAuthenticator` is a simple authenticator that +allows for any username/password unless a global password has been set. If set, it will allow for any username as long as the correct password is provided. To set a global password, add this to the config file: diff --git a/docs/source/getting-started/config-basics.md b/docs/source/getting-started/config-basics.md index f1438fd7..8b4babe6 100644 --- a/docs/source/getting-started/config-basics.md +++ b/docs/source/getting-started/config-basics.md @@ -44,7 +44,7 @@ jupyterhub -f /etc/jupyterhub/jupyterhub_config.py ``` The IPython documentation provides additional information on the -[config system](http://ipython.readthedocs.io/en/stable/development/config) +[config system](http://ipython.readthedocs.io/en/stable/development/config.html) that Jupyter uses. ## Configure using command line options @@ -63,11 +63,11 @@ would enter: jupyterhub --ip 10.0.1.2 --port 443 --ssl-key my_ssl.key --ssl-cert my_ssl.cert ``` -All configurable options may technically be set on the command-line, +All configurable options may technically be set on the command line, though some are inconvenient to type. To set a particular configuration parameter, `c.Class.trait`, you would use the command line option, `--Class.trait`, when starting JupyterHub. For example, to configure the -`c.Spawner.notebook_dir` trait from the command-line, use the +`c.Spawner.notebook_dir` trait from the command line, use the `--Spawner.notebook_dir` option: ```bash @@ -89,11 +89,11 @@ meant as illustration, are: ## Run the proxy separately This is _not_ strictly necessary, but useful in many cases. If you -use a custom proxy (e.g. Traefik), this also not needed. +use a custom proxy (e.g. Traefik), this is also not needed. Connections to user servers go through the proxy, and _not_ the hub itself. If the proxy stays running when the hub restarts (for -maintenance, re-configuration, etc.), then use connections are not +maintenance, re-configuration, etc.), then user connections are not interrupted. For simplicity, by default the hub starts the proxy automatically, so if the hub restarts, the proxy restarts, and user connections are interrupted. It is easy to run the proxy separately, diff --git a/docs/source/getting-started/faq.md b/docs/source/getting-started/faq.md index e5cd30af..d9b20fe6 100644 --- a/docs/source/getting-started/faq.md +++ b/docs/source/getting-started/faq.md @@ -26,7 +26,7 @@ so Breq would open `/user/breq/notebooks/foo.ipynb` and Seivarden would open `/user/seivarden/notebooks/foo.ipynb`, etc. JupyterHub has a special URL that does exactly this! -It's called `/hub/user-redirect/...` and after the visitor logs in, +It's called `/hub/user-redirect/...`. So if you replace `/user/yourname` in your URL bar with `/hub/user-redirect` any visitor should get the same URL on their own server, rather than visiting yours. diff --git a/docs/source/getting-started/institutional-faq.md b/docs/source/getting-started/institutional-faq.md index c5e88798..d7f1a5dd 100644 --- a/docs/source/getting-started/institutional-faq.md +++ b/docs/source/getting-started/institutional-faq.md @@ -11,7 +11,7 @@ Yes! JupyterHub has been used at-scale for large pools of users, as well as complex and high-performance computing. For example, UC Berkeley uses JupyterHub for its Data Science Education Program courses (serving over 3,000 students). The Pangeo project uses JupyterHub to provide access -to scalable cloud computing with Dask. JupyterHub is stable customizable +to scalable cloud computing with Dask. JupyterHub is stable and customizable to the use-cases of large organizations. ### I keep hearing about Jupyter Notebook, JupyterLab, and now JupyterHub. What’s the difference? @@ -27,14 +27,14 @@ Here is a quick breakdown of these three tools: for other parts of the data science stack. - **JupyterHub** is an application that manages interactive computing sessions for **multiple users**. It also connects them with infrastructure those users wish to access. It can provide - remote access to Jupyter Notebooks and Jupyter Lab for many people. + remote access to Jupyter Notebooks and JupyterLab for many people. ## For management ### Briefly, what problem does JupyterHub solve for us? JupyterHub provides a shared platform for data science and collaboration. -It allows users to utilize familiar data science workflows (such as the scientific python stack, +It allows users to utilize familiar data science workflows (such as the scientific Python stack, the R tidyverse, and Jupyter Notebooks) on institutional infrastructure. It also allows administrators some control over access to resources, security, environments, and authentication. @@ -55,7 +55,7 @@ industry, and government research labs. It is most-commonly used by two kinds of - Large teams (e.g., a department, a large class, or a large group of remote users) to provide access to organizational hardware, data, and analytics environments at scale. -Here are a sample of organizations that use JupyterHub: +Here is a sample of organizations that use JupyterHub: - **Universities and colleges**: UC Berkeley, UC San Diego, Cal Poly SLO, Harvard University, University of Chicago, University of Oslo, University of Sheffield, Université Paris Sud, University of Versailles @@ -99,7 +99,7 @@ that we currently suggest are: guide that runs on Kubernetes. Better for larger or dynamic user groups (50-10,000) or more complex compute/data needs. - [The Littlest JupyterHub](https://tljh.jupyter.org) is a lightweight JupyterHub that runs on a single - single machine (in the cloud or under your desk). Better for smaller usergroups (4-80) or more + single machine (in the cloud or under your desk). Better for smaller user groups (4-80) or more lightweight computational resources. ### Does JupyterHub run well in the cloud? @@ -123,7 +123,7 @@ level for several years, and makes a number of "default" security decisions that users. - For security considerations in the base JupyterHub application, - [see the JupyterHub security page](https://jupyterhub.readthedocs.io/en/stable/reference/websecurity.html) + [see the JupyterHub security page](https://jupyterhub.readthedocs.io/en/stable/reference/websecurity.html). - For security considerations when deploying JupyterHub on Kubernetes, see the [JupyterHub on Kubernetes security page](https://zero-to-jupyterhub.readthedocs.io/en/latest/security.html). @@ -183,7 +183,7 @@ how those resources are controlled is taken care of by the non-JupyterHub applic Yes - JupyterHub can provide access to many kinds of computing infrastructure. Especially when combined with other open-source schedulers such as Dask, you can manage fairly -complex computing infrastructure from the interactive sessions of a JupyterHub. For example +complex computing infrastructures from the interactive sessions of a JupyterHub. For example [see the Dask HPC page](https://docs.dask.org/en/latest/setup/hpc.html). ### How much resources do user sessions take? @@ -192,7 +192,7 @@ This is highly configurable by the administrator. If you wish for your users to data analytics environments for prototyping and light data exploring, you can restrict their memory and CPU based on the resources that you have available. If you'd like your JupyterHub to serve as a gateway to high-performance compute or data resources, you may increase the -resources available on user machines, or connect them with computing infrastructure elsewhere. +resources available on user machines, or connect them with computing infrastructures elsewhere. ### Can I customize the look and feel of a JupyterHub? @@ -217,7 +217,7 @@ your JupyterHub with the various services and tools that you wish to provide to ### How well does JupyterHub scale? What are JupyterHub's limitations? JupyterHub works well at both a small scale (e.g., a single VM or machine) as well as a -high scale (e.g., a scalable Kubernetes cluster). It can be used for teams as small a 2, and +high scale (e.g., a scalable Kubernetes cluster). It can be used for teams as small as 2, and for user bases as large as 10,000. The scalability of JupyterHub largely depends on the infrastructure on which it is deployed. JupyterHub has been designed to be lightweight and flexible, so you can tailor your JupyterHub deployment to your needs. @@ -249,7 +249,7 @@ share their results with one another. JupyterHub also provides a computational framework to share computational narratives between different levels of an organization. For example, data scientists can share Jupyter Notebooks -rendered as [voila dashboards](https://voila.readthedocs.io/en/stable/) with those who are not +rendered as [Voilà dashboards](https://voila.readthedocs.io/en/stable/) with those who are not familiar with programming, or create publicly-available interactive analyses to allow others to interact with your work. diff --git a/docs/source/getting-started/networking-basics.md b/docs/source/getting-started/networking-basics.md index b844afe3..6d4d62d9 100644 --- a/docs/source/getting-started/networking-basics.md +++ b/docs/source/getting-started/networking-basics.md @@ -43,7 +43,7 @@ port. By default, this REST API listens on port 8001 of `localhost` only. The Hub service talks to the proxy via a REST API on a secondary port. The -API URL can be configured separately and override the default settings. +API URL can be configured separately to override the default settings. ### Set api_url @@ -82,13 +82,13 @@ c.JupyterHub.hub_ip = '10.0.1.4' c.JupyterHub.hub_port = 54321 ``` -**Added in 0.8:** The `c.JupyterHub.hub_connect_ip` setting is the ip address or +**Added in 0.8:** The `c.JupyterHub.hub_connect_ip` setting is the IP address or hostname that other services should use to connect to the Hub. A common configuration for, e.g. docker, is: ```python c.JupyterHub.hub_ip = '0.0.0.0' # listen on all interfaces -c.JupyterHub.hub_connect_ip = '10.0.1.4' # ip as seen on the docker network. Can also be a hostname. +c.JupyterHub.hub_connect_ip = '10.0.1.4' # IP as seen on the docker network. Can also be a hostname. ``` ## Adjusting the hub's URL diff --git a/docs/source/getting-started/services-basics.md b/docs/source/getting-started/services-basics.md index 5db9a6cb..ba472fc8 100644 --- a/docs/source/getting-started/services-basics.md +++ b/docs/source/getting-started/services-basics.md @@ -2,7 +2,7 @@ When working with JupyterHub, a **Service** is defined as a process that interacts with the Hub's REST API. A Service may perform a specific -or action or task. For example, shutting down individuals' single user +action or task. For example, shutting down individuals' single user notebook servers that have been idle for some time is a good example of a task that could be automated by a Service. Let's look at how the [jupyterhub_idle_culler][] script can be used as a Service. @@ -114,7 +114,7 @@ interact with it. This will run the idle culler service manually. It can be run as a standalone script anywhere with access to the Hub, and will periodically check for idle servers and shut them down via the Hub's REST API. In order to shutdown the -servers, the token given to cull-idle must have admin privileges. +servers, the token given to `cull-idle` must have admin privileges. Generate an API token and store it in the `JUPYTERHUB_API_TOKEN` environment variable. Run `jupyterhub_idle_culler` manually. diff --git a/docs/source/getting-started/spawners-basics.md b/docs/source/getting-started/spawners-basics.md index c30d89f6..9988c2f8 100644 --- a/docs/source/getting-started/spawners-basics.md +++ b/docs/source/getting-started/spawners-basics.md @@ -1,8 +1,8 @@ # Spawners and single-user notebook servers Since the single-user server is an instance of `jupyter notebook`, an entire separate -multi-process application, there are many aspect of that server can configure, and a lot of ways -to express that configuration. +multi-process application, there are many aspects of that server that can be configured, and a lot +of ways to express that configuration. At the JupyterHub level, you can set some values on the Spawner. The simplest of these is `Spawner.notebook_dir`, which lets you set the root directory for a user's server. This root @@ -14,7 +14,7 @@ expanded to the user's home directory. c.Spawner.notebook_dir = '~/notebooks' ``` -You can also specify extra command-line arguments to the notebook server with: +You can also specify extra command line arguments to the notebook server with: ```python c.Spawner.args = ['--debug', '--profile=PHYS131'] diff --git a/docs/source/index.rst b/docs/source/index.rst index 8d51a6bc..f5d2c072 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -115,8 +115,8 @@ We want you to contribute to JupyterHub in ways that are most exciting & useful to you. We value documentation, testing, bug reporting & code equally, and are glad to have your contributions in whatever form you wish :) -Our `Code of Conduct `_ -(`reporting guidelines `_) +Our `Code of Conduct `_ +(`reporting guidelines `_) helps keep our community welcoming to as many people as possible. .. toctree:: @@ -147,4 +147,4 @@ Questions? Suggestions? .. _JupyterHub: https://github.com/jupyterhub/jupyterhub .. _Jupyter notebook: https://jupyter-notebook.readthedocs.io/en/latest/ -.. _REST API: http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyterhub/jupyterhub/master/docs/rest-api.yml#!/default +.. _REST API: https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/jupyterhub/jupyterhub/HEAD/docs/rest-api.yml#!/default diff --git a/docs/source/installation-guide-hard.rst b/docs/source/installation-guide-hard.rst index 99569d3f..b24cea08 100644 --- a/docs/source/installation-guide-hard.rst +++ b/docs/source/installation-guide-hard.rst @@ -3,4 +3,4 @@ JupyterHub the hard way ======================= -This guide has moved to https://github.com/jupyterhub/jupyterhub-the-hard-way/blob/master/docs/installation-guide-hard.md +This guide has moved to https://github.com/jupyterhub/jupyterhub-the-hard-way/blob/HEAD/docs/installation-guide-hard.md diff --git a/docs/source/reference/authenticators.md b/docs/source/reference/authenticators.md index e66e0b98..c1899922 100644 --- a/docs/source/reference/authenticators.md +++ b/docs/source/reference/authenticators.md @@ -259,7 +259,7 @@ PAM session. Beginning with version 0.8, JupyterHub is an OAuth provider. -[authenticator]: https://github.com/jupyterhub/jupyterhub/blob/master/jupyterhub/auth.py +[authenticator]: https://github.com/jupyterhub/jupyterhub/blob/HEAD/jupyterhub/auth.py [pam]: https://en.wikipedia.org/wiki/Pluggable_authentication_module [oauth]: https://en.wikipedia.org/wiki/OAuth [github oauth]: https://developer.github.com/v3/oauth/ diff --git a/docs/source/reference/rest.md b/docs/source/reference/rest.md index 4b040ec1..7612b55d 100644 --- a/docs/source/reference/rest.md +++ b/docs/source/reference/rest.md @@ -209,7 +209,7 @@ be viewed in a more [interactive style on swagger's petstore][]. Both resources contain the same information and differ only in its display. Note: The Swagger specification is being renamed the [OpenAPI Initiative][]. -[interactive style on swagger's petstore]: http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyterhub/jupyterhub/master/docs/rest-api.yml#!/default +[interactive style on swagger's petstore]: https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/jupyterhub/jupyterhub/HEAD/docs/rest-api.yml#!/default [openapi initiative]: https://www.openapis.org/ [jupyterhub rest api]: ./rest-api -[jupyter notebook rest api]: http://petstore.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/master/notebook/services/api/api.yaml +[jupyter notebook rest api]: https://petstore3.swagger.io/?url=https://raw.githubusercontent.com/jupyter/notebook/HEAD/notebook/services/api/api.yaml diff --git a/docs/source/reference/services.md b/docs/source/reference/services.md index f3f0e83f..8563dc25 100644 --- a/docs/source/reference/services.md +++ b/docs/source/reference/services.md @@ -230,7 +230,7 @@ configurable by the `cookie_cache_max_age` setting (default: five minutes). For example, you have a Flask service that returns information about a user. JupyterHub's HubAuth class can be used to authenticate requests to the Flask service. See the `service-whoami-flask` example in the -[JupyterHub GitHub repo](https://github.com/jupyterhub/jupyterhub/tree/master/examples/service-whoami-flask) +[JupyterHub GitHub repo](https://github.com/jupyterhub/jupyterhub/tree/HEAD/examples/service-whoami-flask) for more details. ```python diff --git a/docs/source/reference/spawners.md b/docs/source/reference/spawners.md index 562687bf..b3fa15c5 100644 --- a/docs/source/reference/spawners.md +++ b/docs/source/reference/spawners.md @@ -125,7 +125,7 @@ If the `Spawner.options_form` is defined, when a user tries to start their serve If `Spawner.options_form` is undefined, the user's server is spawned directly, and no spawn page is rendered. -See [this example](https://github.com/jupyterhub/jupyterhub/blob/master/examples/spawn-form/jupyterhub_config.py) for a form that allows custom CLI args for the local spawner. +See [this example](https://github.com/jupyterhub/jupyterhub/blob/HEAD/examples/spawn-form/jupyterhub_config.py) for a form that allows custom CLI args for the local spawner. ### `Spawner.options_from_form` @@ -166,7 +166,7 @@ which would return: When `Spawner.start` is called, this dictionary is accessible as `self.user_options`. -[spawner]: https://github.com/jupyterhub/jupyterhub/blob/master/jupyterhub/spawner.py +[spawner]: https://github.com/jupyterhub/jupyterhub/blob/HEAD/jupyterhub/spawner.py ## Writing a custom spawner diff --git a/docs/source/reference/templates.md b/docs/source/reference/templates.md index 38b7f0a0..1509e0f2 100644 --- a/docs/source/reference/templates.md +++ b/docs/source/reference/templates.md @@ -10,7 +10,7 @@ appearance. JupyterHub will look for custom templates in all of the paths in the `JupyterHub.template_paths` configuration option, falling back on the -[default templates](https://github.com/jupyterhub/jupyterhub/tree/master/share/jupyterhub/templates) +[default templates](https://github.com/jupyterhub/jupyterhub/tree/HEAD/share/jupyterhub/templates) if no custom template with that name is found. This fallback behavior is new in version 0.9; previous versions searched only those paths explicitly included in `template_paths`. You may override as many @@ -21,7 +21,7 @@ or as few templates as you desire. Jinja provides a mechanism to [extend templates](http://jinja.pocoo.org/docs/2.10/templates/#template-inheritance). A base template can define a `block`, and child templates can replace or supplement the material in the block. The -[JupyterHub templates](https://github.com/jupyterhub/jupyterhub/tree/master/share/jupyterhub/templates) +[JupyterHub templates](https://github.com/jupyterhub/jupyterhub/tree/HEAD/share/jupyterhub/templates) make extensive use of blocks, which allows you to customize parts of the interface easily. diff --git a/docs/source/troubleshooting.md b/docs/source/troubleshooting.md index 3623c1b3..898b46c8 100644 --- a/docs/source/troubleshooting.md +++ b/docs/source/troubleshooting.md @@ -234,7 +234,7 @@ With a docker container, pass in the environment variable with the run command: -e JUPYTERHUB_API_TOKEN=my_secret_token \ jupyter/datascience-notebook:latest -[This example](https://github.com/jupyterhub/jupyterhub/tree/master/examples/service-notebook/external) demonstrates how to combine the use of the `jupyterhub-singleuser` environment variables when launching a Notebook as an externally managed service. +[This example](https://github.com/jupyterhub/jupyterhub/tree/HEAD/examples/service-notebook/external) demonstrates how to combine the use of the `jupyterhub-singleuser` environment variables when launching a Notebook as an externally managed service. ## How do I...? diff --git a/examples/bootstrap-script/README.md b/examples/bootstrap-script/README.md index ed7fe6ff..9fa85b9f 100644 --- a/examples/bootstrap-script/README.md +++ b/examples/bootstrap-script/README.md @@ -148,9 +148,9 @@ else echo "...initial content loading for user ..." mkdir $USER_DIRECTORY/tutorials cd $USER_DIRECTORY/tutorials - wget https://github.com/jakevdp/PythonDataScienceHandbook/archive/master.zip - unzip -o master.zip - rm master.zip + wget https://github.com/jakevdp/PythonDataScienceHandbook/archive/HEAD.zip + unzip -o HEAD.zip + rm HEAD.zip fi exit 0 diff --git a/examples/bootstrap-script/bootstrap.sh b/examples/bootstrap-script/bootstrap.sh index 417d8463..f095d463 100755 --- a/examples/bootstrap-script/bootstrap.sh +++ b/examples/bootstrap-script/bootstrap.sh @@ -40,9 +40,9 @@ else echo "...initial content loading for user ..." mkdir $USER_DIRECTORY/tutorials cd $USER_DIRECTORY/tutorials - wget https://github.com/jakevdp/PythonDataScienceHandbook/archive/master.zip - unzip -o master.zip - rm master.zip + wget https://github.com/jakevdp/PythonDataScienceHandbook/archive/HEAD.zip + unzip -o HEAD.zip + rm HEAD.zip fi exit 0 diff --git a/jupyterhub/_version.py b/jupyterhub/_version.py index ff94410c..203c3178 100644 --- a/jupyterhub/_version.py +++ b/jupyterhub/_version.py @@ -5,7 +5,7 @@ version_info = ( 1, 4, - 0, + 1, "", # release (b1, rc1, or "" for final or dev) # "dev", # dev or nothing for beta/rc/stable releases ) diff --git a/jupyterhub/apihandlers/users.py b/jupyterhub/apihandlers/users.py index 27e9bf0f..4b852e88 100644 --- a/jupyterhub/apihandlers/users.py +++ b/jupyterhub/apihandlers/users.py @@ -238,11 +238,7 @@ class UserAPIHandler(APIHandler): await maybe_future(self.authenticator.delete_user(user)) - # allow the spawner to cleanup any persistent resources associated with the user - try: - await user.spawner.delete_forever() - except Exception as e: - self.log.error("Error cleaning up persistent resources: %s" % e) + await user.delete_spawners() # remove from registry self.users.delete(user) @@ -484,10 +480,18 @@ class UserServerAPIHandler(APIHandler): options = self.get_json_body() remove = (options or {}).get('remove', False) - def _remove_spawner(f=None): - if f and f.exception(): - return + async def _remove_spawner(f=None): + """Remove the spawner object + + only called after it stops successfully + """ + if f: + # await f, stop on error, + # leaving resources in the db in case of failure to stop + await f self.log.info("Deleting spawner %s", spawner._log_name) + await maybe_future(user._delete_spawner(spawner)) + self.db.delete(spawner.orm_spawner) user.spawners.pop(server_name, None) self.db.commit() @@ -508,7 +512,8 @@ class UserServerAPIHandler(APIHandler): self.set_header('Content-Type', 'text/plain') self.set_status(202) if remove: - spawner._stop_future.add_done_callback(_remove_spawner) + # schedule remove when stop completes + asyncio.ensure_future(_remove_spawner(spawner._stop_future)) return if spawner.pending: @@ -526,9 +531,10 @@ class UserServerAPIHandler(APIHandler): if remove: if stop_future: - stop_future.add_done_callback(_remove_spawner) + # schedule remove when stop completes + asyncio.ensure_future(_remove_spawner(spawner._stop_future)) else: - _remove_spawner() + await _remove_spawner() status = 202 if spawner._stop_pending else 204 self.set_header('Content-Type', 'text/plain') diff --git a/jupyterhub/proxy.py b/jupyterhub/proxy.py index 82587191..5f706970 100644 --- a/jupyterhub/proxy.py +++ b/jupyterhub/proxy.py @@ -24,6 +24,7 @@ import time from functools import wraps from subprocess import Popen from urllib.parse import quote +from weakref import WeakKeyDictionary from tornado.httpclient import AsyncHTTPClient from tornado.httpclient import HTTPError @@ -44,7 +45,6 @@ from .metrics import CHECK_ROUTES_DURATION_SECONDS from .metrics import PROXY_POLL_DURATION_SECONDS from .objects import Server from .utils import exponential_backoff -from .utils import make_ssl_context from .utils import url_path_join from jupyterhub.traitlets import Command @@ -55,11 +55,18 @@ def _one_at_a_time(method): If multiple concurrent calls to this method are made, queue them instead of allowing them to be concurrently outstanding. """ - method._lock = asyncio.Lock() + # use weak dict for locks + # so that the lock is always acquired within the current asyncio loop + # should only be relevant in testing, where eventloops are created and destroyed often + method._locks = WeakKeyDictionary() @wraps(method) async def locked_method(*args, **kwargs): - async with method._lock: + loop = asyncio.get_event_loop() + lock = method._locks.get(loop, None) + if lock is None: + lock = method._locks[loop] = asyncio.Lock() + async with lock: return await method(*args, **kwargs) return locked_method @@ -123,7 +130,7 @@ class Proxy(LoggingConfigurable): a URL as target. The hub will ensure this route is present in the proxy. - If the hub is running in host based mode (with + If the hub is running in host based mode (with JupyterHub.subdomain_host set), the routespec *must* have a domain component (example.com/my-url/). If the hub is not running in host based mode, the routespec diff --git a/jupyterhub/singleuser/mixins.py b/jupyterhub/singleuser/mixins.py index 0e5bc06f..769f3148 100755 --- a/jupyterhub/singleuser/mixins.py +++ b/jupyterhub/singleuser/mixins.py @@ -10,9 +10,11 @@ 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 secrets +import sys import warnings from datetime import datetime from datetime import timezone @@ -99,19 +101,26 @@ class JupyterHubLoginHandlerMixin: Thus shouldn't be called anymore because HubAuthenticatedHandler should have already overridden get_current_user(). - Keep here to prevent unlikely circumstance from losing auth. + Keep here to protect uncommon circumstance of multiple BaseHandlers + from missing auth. + + e.g. when multiple BaseHandler classes are used. """ if HubAuthenticatedHandler not in handler.__class__.mro(): warnings.warn( - f"Expected to see HubAuthenticatedHandler in {handler.__class__}.mro()", + f"Expected to see HubAuthenticatedHandler in {handler.__class__}.mro()," + " patching in at call time. Hub authentication is still applied.", RuntimeWarning, stacklevel=2, ) + # patch HubAuthenticated into the instance handler.__class__ = type( handler.__class__.__name__, (HubAuthenticatedHandler, handler.__class__), {}, ) + # patch into the class itself so this doesn't happen again for the same class + patch_base_handler(handler.__class__) return handler.get_current_user() @classmethod @@ -683,6 +692,97 @@ def detect_base_package(App): return None +def _nice_cls_repr(cls): + """Nice repr of classes, e.g. 'module.submod.Class' + + Also accepts tuples of classes + """ + return f"{cls.__module__}.{cls.__name__}" + + +def patch_base_handler(BaseHandler, log=None): + """Patch HubAuthenticated into a base handler class + + so anything inheriting from BaseHandler uses Hub authentication. + This works *even after* subclasses have imported and inherited from BaseHandler. + + .. versionadded: 1.5 + Made available as an importable utility + """ + if log is None: + log = logging.getLogger() + + if HubAuthenticatedHandler not in BaseHandler.__bases__: + new_bases = (HubAuthenticatedHandler,) + BaseHandler.__bases__ + log.info( + "Patching auth into {mod}.{name}({old_bases}) -> {name}({new_bases})".format( + mod=BaseHandler.__module__, + name=BaseHandler.__name__, + old_bases=', '.join( + _nice_cls_repr(cls) for cls in BaseHandler.__bases__ + ), + new_bases=', '.join(_nice_cls_repr(cls) for cls in new_bases), + ) + ) + BaseHandler.__bases__ = new_bases + # We've now inserted our class as a parent of BaseHandler, + # but we also need to ensure BaseHandler *itself* doesn't + # override the public tornado API methods we have inserted. + # If they are defined in BaseHandler, explicitly replace them with our methods. + for name in ("get_current_user", "get_login_url"): + if name in BaseHandler.__dict__: + log.debug( + f"Overriding {BaseHandler}.{name} with HubAuthenticatedHandler.{name}" + ) + method = getattr(HubAuthenticatedHandler, name) + setattr(BaseHandler, name, method) + return BaseHandler + + +def _patch_app_base_handlers(app): + """Patch Hub Authentication into the base handlers of an app + + Patches HubAuthenticatedHandler into: + + - App.base_handler_class (if defined) + - jupyter_server's JupyterHandler (if already imported) + - notebook's IPythonHandler (if already imported) + """ + BaseHandler = app_base_handler = getattr(app, "base_handler_class", None) + + base_handlers = [] + if BaseHandler is not None: + base_handlers.append(BaseHandler) + + # patch juptyer_server and notebook handlers if they have been imported + for base_handler_name in [ + "jupyter_server.base.handlers.JupyterHandler", + "notebook.base.handlers.IPythonHandler", + ]: + modname, _ = base_handler_name.rsplit(".", 1) + if modname in sys.modules: + base_handlers.append(import_item(base_handler_name)) + + if not base_handlers: + pkg = detect_base_package(app.__class__) + if pkg == "jupyter_server": + BaseHandler = import_item("jupyter_server.base.handlers.JupyterHandler") + elif pkg == "notebook": + BaseHandler = import_item("notebook.base.handlers.IPythonHandler") + else: + raise ValueError( + "{}.base_handler_class must be defined".format(app.__class__.__name__) + ) + base_handlers.append(BaseHandler) + + # patch-in HubAuthenticatedHandler to base handler classes + for BaseHandler in base_handlers: + patch_base_handler(BaseHandler) + + # return the first entry + return base_handlers[0] + + def make_singleuser_app(App): """Make and return a singleuser notebook app @@ -706,37 +806,7 @@ def make_singleuser_app(App): # detect base classes LoginHandler = empty_parent_app.login_handler_class LogoutHandler = empty_parent_app.logout_handler_class - BaseHandler = getattr(empty_parent_app, "base_handler_class", None) - if BaseHandler is None: - pkg = detect_base_package(App) - if pkg == "jupyter_server": - BaseHandler = import_item("jupyter_server.base.handlers.JupyterHandler") - elif pkg == "notebook": - BaseHandler = import_item("notebook.base.handlers.IPythonHandler") - else: - raise ValueError( - "{}.base_handler_class must be defined".format(App.__name__) - ) - - # patch-in HubAuthenticatedHandler to BaseHandler, - # so anything inheriting from BaseHandler uses Hub authentication - if HubAuthenticatedHandler not in BaseHandler.__bases__: - new_bases = (HubAuthenticatedHandler,) + BaseHandler.__bases__ - log.debug( - f"Patching {BaseHandler}{BaseHandler.__bases__} -> {BaseHandler}{new_bases}" - ) - BaseHandler.__bases__ = new_bases - # We've now inserted our class as a parent of BaseHandler, - # but we also need to ensure BaseHandler *itself* doesn't - # override the public tornado API methods we have inserted. - # If they are defined in BaseHandler, explicitly replace them with our methods. - for name in ("get_current_user", "get_login_url"): - if name in BaseHandler.__dict__: - log.debug( - f"Overriding {BaseHandler}.{name} with HubAuthenticatedHandler.{name}" - ) - method = getattr(HubAuthenticatedHandler, name) - setattr(BaseHandler, name, method) + BaseHandler = _patch_app_base_handlers(empty_parent_app) # create Handler classes from mixins + bases class JupyterHubLoginHandler(JupyterHubLoginHandlerMixin, LoginHandler): @@ -766,4 +836,11 @@ def make_singleuser_app(App): logout_handler_class = JupyterHubLogoutHandler oauth_callback_handler_class = OAuthCallbackHandler + def initialize(self, *args, **kwargs): + result = super().initialize(*args, **kwargs) + # run patch again after initialize, so extensions have already been loaded + # probably a no-op most of the time + _patch_app_base_handlers(self) + return result + return SingleUserNotebookApp diff --git a/jupyterhub/spawner.py b/jupyterhub/spawner.py index ead537d6..c296ff86 100644 --- a/jupyterhub/spawner.py +++ b/jupyterhub/spawner.py @@ -1141,6 +1141,18 @@ class Spawner(LoggingConfigurable): """ raise NotImplementedError("Override in subclass. Must be a coroutine.") + def delete_forever(self): + """Called when a user or server is deleted. + + This can do things like request removal of resources such as persistent storage. + Only called on stopped spawners, and is usually the last action ever taken for the user. + + Will only be called once on each Spawner, immediately prior to removal. + + Stopping a server does *not* call this method. + """ + pass + def add_poll_callback(self, callback, *args, **kwargs): """Add a callback to fire when the single-user server stops""" if args or kwargs: diff --git a/jupyterhub/tests/mocking.py b/jupyterhub/tests/mocking.py index 145ef2a9..5384ae11 100644 --- a/jupyterhub/tests/mocking.py +++ b/jupyterhub/tests/mocking.py @@ -93,16 +93,6 @@ class MockSpawner(SimpleLocalProcessSpawner): def _cmd_default(self): return [sys.executable, '-m', 'jupyterhub.tests.mocksu'] - async def delete_forever(self): - """Called when a user is deleted. - - This can do things like request removal of resources such as persistent storage. - Only called on stopped spawners, and is likely the last action ever taken for the user. - - Will only be called once on the user's default Spawner. - """ - pass - use_this_api_token = None def start(self): diff --git a/jupyterhub/user.py b/jupyterhub/user.py index ce1dc300..93c845f8 100644 --- a/jupyterhub/user.py +++ b/jupyterhub/user.py @@ -252,6 +252,35 @@ class User: await self.save_auth_state(auth_state) return auth_state + async def delete_spawners(self): + """Call spawner cleanup methods + + Allows the spawner to cleanup persistent resources + """ + for name in self.orm_user.orm_spawners.keys(): + await self._delete_spawner(name) + + async def _delete_spawner(self, name_or_spawner): + """Delete a single spawner""" + # always ensure full Spawner + # this may instantiate the Spawner if it wasn't already running, + # just to delete it + if isinstance(name_or_spawner, str): + spawner = self.spawners[name_or_spawner] + else: + spawner = name_or_spawner + + if spawner.active: + raise RuntimeError( + f"Spawner {spawner._log_name} is active and cannot be deleted." + ) + try: + await maybe_future(spawner.delete_forever()) + except Exception as e: + self.log.exception( + f"Error cleaning up persistent resources on {spawner._log_name}" + ) + def all_spawners(self, include_default=True): """Generator yielding all my spawners diff --git a/singleuser/Dockerfile b/singleuser/Dockerfile index 6b9115f2..61a22cd1 100644 --- a/singleuser/Dockerfile +++ b/singleuser/Dockerfile @@ -6,7 +6,7 @@ FROM $BASE_IMAGE MAINTAINER Project Jupyter ADD install_jupyterhub /tmp/install_jupyterhub -ARG JUPYTERHUB_VERSION=master +ARG JUPYTERHUB_VERSION=main # install pinned jupyterhub and ensure notebook is installed RUN python3 /tmp/install_jupyterhub && \ python3 -m pip install notebook diff --git a/singleuser/README.md b/singleuser/README.md index c39dcf78..c47d6270 100644 --- a/singleuser/README.md +++ b/singleuser/README.md @@ -5,7 +5,7 @@ Built from the `jupyter/base-notebook` base image. This image contains a single user notebook server for use with [JupyterHub](https://github.com/jupyterhub/jupyterhub). In particular, it is meant to be used with the -[DockerSpawner](https://github.com/jupyterhub/dockerspawner/blob/master/dockerspawner/dockerspawner.py) +[DockerSpawner](https://github.com/jupyterhub/dockerspawner/blob/HEAD/dockerspawner/dockerspawner.py) class to launch user notebook servers within docker containers. The only thing this image accomplishes is pinning the jupyterhub version on top of base-notebook. diff --git a/singleuser/hooks/post_push b/singleuser/hooks/post_push index 616d2525..91db9e4c 100644 --- a/singleuser/hooks/post_push +++ b/singleuser/hooks/post_push @@ -13,7 +13,7 @@ function get_hub_version() { hub_xy="${hub_xy}.${split[3]}" fi } -# tag e.g. 0.9 with master +# 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 diff --git a/singleuser/install_jupyterhub b/singleuser/install_jupyterhub index b4ea2d42..b76c4237 100644 --- a/singleuser/install_jupyterhub +++ b/singleuser/install_jupyterhub @@ -9,8 +9,8 @@ pip_install = [ sys.executable, '-m', 'pip', 'install', '--no-cache', '--upgrade', '--upgrade-strategy', 'only-if-needed', ] -if V == 'master': - req = 'https://github.com/jupyterhub/jupyterhub/archive/master.tar.gz' +if V in {'main', 'HEAD'}: + req = 'https://github.com/jupyterhub/jupyterhub/archive/HEAD.tar.gz' else: version_info = [ int(part) for part in V.split('.') ] version_info[-1] += 1