diff --git a/jupyterhub_V1/README.md b/jupyterhub_V1/README.md new file mode 100644 index 0000000..6b4987a --- /dev/null +++ b/jupyterhub_V1/README.md @@ -0,0 +1,5 @@ +# JupyterHub Docker Stacks + +## Acknowledgement + +[Jupyter Docker Stacks](https://github.com/jupyter/docker-stacks) \ No newline at end of file diff --git a/jupyterhub_V1/jupyterhub-pytorch/Dockerfile b/jupyterhub_V1/jupyterhub-pytorch/Dockerfile new file mode 100644 index 0000000..fd5391e --- /dev/null +++ b/jupyterhub_V1/jupyterhub-pytorch/Dockerfile @@ -0,0 +1,26 @@ +ARG SOURCE=ghcr.io +ARG BASE_CONTAINER=${SOURCE}/jupyter-base:0.0.3 +FROM $BASE_CONTAINER + +LABEL org.opencontainers.image.authors="Bayes Cluster Maintenance Group" +LABEL org.opencontainers.image.documentation="https://uicstat.com" + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +USER root + +# Install all OS dependencies for fully functional notebook server +RUN apt-get update --yes && \ + apt-get install --yes --no-install-recommends \ + git \ + nano-tiny \ + tzdata \ + unzip \ + vim-tiny \ + openssh-client \ + less \ + texlive-xetex \ + texlive-fonts-recommended \ + texlive-plain-generic \ + xclip && \ + apt-get clean && rm -rf /var/lib/apt/lists/* \ No newline at end of file diff --git a/jupyterhub_V1/singleuser-nvidia/Dockerfile b/jupyterhub_V1/singleuser-nvidia/Dockerfile new file mode 100644 index 0000000..5325111 --- /dev/null +++ b/jupyterhub_V1/singleuser-nvidia/Dockerfile @@ -0,0 +1,94 @@ +FROM nvidia/cuda:11.8.0-runtime-ubuntu22.04 + +LABEL maintainer="Bayes Cluster Maintenance Group <" +ARG NB_USER="bayes" +ARG NB_UID="1000" +ARG NB_GID="100" + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +# Install all OS dependencies for fully functional notebook server +USER root +ENV DEBIAN_FRONTEND noninteractive +RUN apt-get update --yes && \ + apt-get upgrade --yes && \ + apt-get install --yes --no-install-recommends \ + bzip2 \ + ca-certificates \ + fonts-liberation \ + locales \ + pandoc \ + run-one \ + sudo \ + tini \ + wget \ + curl && \ + apt-get clean && rm -rf /var/lib/apt/lists/* && \ + echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && \ + locale-gen + +RUN adduser --disabled-password \ + --gecos "Default user" \ + --uid ${NB_UID} \ + ${NB_USER} + +# Configure environment +ENV CONDA_DIR=/opt/conda \ + SHELL=/bin/bash \ + NB_USER="${NB_USER}" \ + NB_UID=${NB_UID} \ + NB_GID=${NB_GID} \ + LC_ALL=en_US.UTF-8 \ + LANG=en_US.UTF-8 \ + LANGUAGE=en_US.UTF-8 +ENV PATH="${CONDA_DIR}/bin:${PATH}" \ + HOME="/home/${NB_USER}" + +COPY fix-permissions /usr/local/bin/fix-permissions +RUN chmod a+rx /usr/local/bin/fix-permissions + +RUN mkdir "/home/${NB_USER}/work" && \ + fix-permissions "/home/${NB_USER}" +RUN fix-permissions "/home/${NB_USER}" + +# Install miniconda +ENV CONDA_URL="https://mirrors.bfsu.edu.cn/anaconda/miniconda/Miniconda3-latest-Linux-x86_64.sh" +RUN curl -LO ${CONDA_URL} +RUN bash Miniconda3-latest-Linux-x86_64.sh -p ${CONDA_DIR} -b +RUN rm -rf Miniconda3-latest-Linux-x86_64.sh +RUN conda config --system --prepend channels conda-forge +RUN chown ${NB_UID}:${NB_GID} ${CONDA_DIR} +RUN conda init +COPY condarc /home/${NB_USER}/.condarc +RUN conda install -c anaconda jupyter +RUN conda install -c conda-forge jupyterlab +RUN conda install -c conda-forge jupyterhub +RUN conda config --system --prepend envs_dirs '/home/${NB_USER}/.conda/envs' +EXPOSE 8888 +ENTRYPOINT ["tini", "--"] +CMD ["jupyter", "lab"] + +# Copy local files as late as possible to avoid cache busting +COPY start.sh start-notebook.sh start-singleuser.sh /usr/local/bin/ +# Currently need to have both jupyter_notebook_config and jupyter_server_config to support classic and lab +COPY jupyter_server_config.py /etc/jupyter/ + +# Fix permissions on /etc/jupyter as root +USER root + +# Legacy for Jupyter Notebook Server, see: [#1205](https://github.com/jupyter/docker-stacks/issues/1205) +RUN sed -re "s/c.ServerApp/c.NotebookApp/g" \ + /etc/jupyter/jupyter_server_config.py > /etc/jupyter/jupyter_notebook_config.py && \ + fix-permissions /etc/jupyter/ + +# HEALTHCHECK documentation: https://docs.docker.com/engine/reference/builder/#healthcheck +# This healtcheck works well for `lab`, `notebook`, `nbclassic`, `server` and `retro` jupyter commands +# https://github.com/jupyter/docker-stacks/issues/915#issuecomment-1068528799 +HEALTHCHECK --interval=15s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -O- --no-verbose --tries=1 --no-check-certificate \ + http${GEN_CERT:+s}://localhost:8888${JUPYTERHUB_SERVICE_PREFIX:-/}api || exit 1 + +# Switch back to jovyan to avoid accidental container runs as root +USER ${NB_UID} + +WORKDIR "${HOME}" \ No newline at end of file diff --git a/jupyterhub_V1/singleuser-nvidia/condarc b/jupyterhub_V1/singleuser-nvidia/condarc new file mode 100644 index 0000000..4aca3f5 --- /dev/null +++ b/jupyterhub_V1/singleuser-nvidia/condarc @@ -0,0 +1,15 @@ +channels: + - defaults +show_channel_urls: true +default_channels: + - https://mirrors.bfsu.edu.cn/anaconda/pkgs/main + - https://mirrors.bfsu.edu.cn/anaconda/pkgs/r + - https://mirrors.bfsu.edu.cn/anaconda/pkgs/msys2 +custom_channels: + conda-forge: https://mirrors.bfsu.edu.cn/anaconda/cloud + msys2: https://mirrors.bfsu.edu.cn/anaconda/cloud + bioconda: https://mirrors.bfsu.edu.cn/anaconda/cloud + menpo: https://mirrors.bfsu.edu.cn/anaconda/cloud + pytorch: https://mirrors.bfsu.edu.cn/anaconda/cloud + pytorch-lts: https://mirrors.bfsu.edu.cn/anaconda/cloud + simpleitk: https://mirrors.bfsu.edu.cn/anaconda/cloud \ No newline at end of file diff --git a/jupyterhub_V1/singleuser-nvidia/fix-permissions b/jupyterhub_V1/singleuser-nvidia/fix-permissions new file mode 100644 index 0000000..472ea28 --- /dev/null +++ b/jupyterhub_V1/singleuser-nvidia/fix-permissions @@ -0,0 +1,35 @@ +#!/bin/bash +# set permissions on a directory +# after any installation, if a directory needs to be (human) user-writable, +# run this script on it. +# It will make everything in the directory owned by the group ${NB_GID} +# and writable by that group. +# Deployments that want to set a specific user id can preserve permissions +# by adding the `--group-add users` line to `docker run`. + +# uses find to avoid touching files that already have the right permissions, +# which would cause massive image explosion + +# right permissions are: +# group=${NB_GID} +# AND permissions include group rwX (directory-execute) +# AND directories have setuid,setgid bits set + +set -e + +for d in "$@"; do + find "${d}" \ + ! \( \ + -group "${NB_GID}" \ + -a -perm -g+rwX \ + \) \ + -exec chgrp "${NB_GID}" -- {} \+ \ + -exec chmod g+rwX -- {} \+ + # setuid, setgid *on directories only* + find "${d}" \ + \( \ + -type d \ + -a ! -perm -6000 \ + \) \ + -exec chmod +6000 -- {} \+ +done \ No newline at end of file diff --git a/jupyterhub_V1/singleuser-nvidia/jupyter_server_config.py b/jupyterhub_V1/singleuser-nvidia/jupyter_server_config.py new file mode 100644 index 0000000..bbf0535 --- /dev/null +++ b/jupyterhub_V1/singleuser-nvidia/jupyter_server_config.py @@ -0,0 +1,60 @@ +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. +# mypy: ignore-errors +import os +import stat +import subprocess + +from jupyter_core.paths import jupyter_data_dir + +c = get_config() # noqa: F821 +c.ServerApp.ip = "0.0.0.0" +c.ServerApp.port = 8888 +c.ServerApp.open_browser = False + +# to output both image/svg+xml and application/pdf plot formats in the notebook file +c.InlineBackend.figure_formats = {"png", "jpeg", "svg", "pdf"} + +# https://github.com/jupyter/notebook/issues/3130 +c.FileContentsManager.delete_to_trash = False + +# Generate a self-signed certificate +OPENSSL_CONFIG = """\ +[req] +distinguished_name = req_distinguished_name +[req_distinguished_name] +""" +if "GEN_CERT" in os.environ: + dir_name = jupyter_data_dir() + pem_file = os.path.join(dir_name, "notebook.pem") + os.makedirs(dir_name, exist_ok=True) + + # Generate an openssl.cnf file to set the distinguished name + cnf_file = os.path.join(os.getenv("CONDA_DIR", "/usr/lib"), "ssl", "openssl.cnf") + if not os.path.isfile(cnf_file): + with open(cnf_file, "w") as fh: + fh.write(OPENSSL_CONFIG) + + # Generate a certificate if one doesn't exist on disk + subprocess.check_call( + [ + "openssl", + "req", + "-new", + "-newkey=rsa:2048", + "-days=365", + "-nodes", + "-x509", + "-subj=/C=XX/ST=XX/L=XX/O=generated/CN=generated", + f"-keyout={pem_file}", + f"-out={pem_file}", + ] + ) + # Restrict access to the file + os.chmod(pem_file, stat.S_IRUSR | stat.S_IWUSR) + c.ServerApp.certfile = pem_file + +# Change default umask for all subprocesses of the notebook server if set in +# the environment +if "NB_UMASK" in os.environ: + os.umask(int(os.environ["NB_UMASK"], 8)) \ No newline at end of file diff --git a/jupyterhub_V1/singleuser-nvidia/requirements.in b/jupyterhub_V1/singleuser-nvidia/requirements.in new file mode 100644 index 0000000..31a4b10 --- /dev/null +++ b/jupyterhub_V1/singleuser-nvidia/requirements.in @@ -0,0 +1,18 @@ +# This file is the input to requirements.txt, +# which is a frozen version of this. To update +# requirements.txt, use the "Run workflow" button at +# https://github.com/jupyterhub/zero-to-jupyterhub-k8s/actions/workflows/watch-dependencies.yaml +# that will also update the jupyterhub version if needed. +# README.md file. + +# JupyterHub itself, update this version pinning by running the workflow +# mentioned above. +jupyterhub==3.0.0 + +# UI +jupyterlab +nbclassic +retrolab + +# plugins +nbgitpuller \ No newline at end of file diff --git a/jupyterhub_V1/singleuser-nvidia/requirements.txt b/jupyterhub_V1/singleuser-nvidia/requirements.txt new file mode 100644 index 0000000..163725e --- /dev/null +++ b/jupyterhub_V1/singleuser-nvidia/requirements.txt @@ -0,0 +1,324 @@ +# +# This file is autogenerated by pip-compile with python 3.9 +# To update, run: +# +# Use the "Run workflow" button at https://github.com/jupyterhub/zero-to-jupyterhub-k8s/actions/workflows/watch-dependencies.yaml +# +alembic==1.8.1 + # via jupyterhub +anyio==3.6.1 + # via jupyter-server +argon2-cffi==21.3.0 + # via + # jupyter-server + # nbclassic + # notebook +argon2-cffi-bindings==21.2.0 + # via argon2-cffi +asttokens==2.0.8 + # via stack-data +async-generator==1.10 + # via jupyterhub +attrs==22.1.0 + # via jsonschema +babel==2.10.3 + # via jupyterlab-server +backcall==0.2.0 + # via ipython +beautifulsoup4==4.11.1 + # via nbconvert +bleach==5.0.1 + # via nbconvert +certifi==2022.9.24 + # via requests +certipy==0.1.3 + # via jupyterhub +cffi==1.15.1 + # via + # argon2-cffi-bindings + # cryptography +charset-normalizer==2.1.1 + # via requests +cryptography==38.0.1 + # via pyopenssl +debugpy==1.6.3 + # via ipykernel +decorator==5.1.1 + # via ipython +defusedxml==0.7.1 + # via nbconvert +entrypoints==0.4 + # via jupyter-client +executing==1.1.0 + # via stack-data +fastjsonschema==2.16.2 + # via nbformat +greenlet==1.1.3 + # via sqlalchemy +idna==3.4 + # via + # anyio + # requests +importlib-metadata==5.0.0 + # via + # jupyterhub + # jupyterlab-server + # nbconvert +ipykernel==6.16.0 + # via + # nbclassic + # notebook +ipython==8.5.0 + # via + # ipykernel + # jupyterlab +ipython-genutils==0.2.0 + # via + # nbclassic + # notebook +jedi==0.18.1 + # via ipython +jinja2==3.1.2 + # via + # jupyter-server + # jupyterhub + # jupyterlab + # jupyterlab-server + # nbclassic + # nbconvert + # notebook +json5==0.9.10 + # via jupyterlab-server +jsonschema==4.16.0 + # via + # jupyter-telemetry + # jupyterlab-server + # nbformat +jupyter-client==7.3.5 + # via + # ipykernel + # jupyter-server + # nbclassic + # nbclient + # notebook +jupyter-core==4.11.1 + # via + # jupyter-client + # jupyter-server + # jupyterlab + # nbclassic + # nbconvert + # nbformat + # notebook +jupyter-server==1.19.1 + # via + # jupyterlab + # jupyterlab-server + # nbclassic + # nbgitpuller + # notebook-shim + # retrolab +jupyter-telemetry==0.1.0 + # via jupyterhub +jupyterhub==3.0.0 + # via -r requirements.in +jupyterlab==3.4.7 + # via + # -r requirements.in + # retrolab +jupyterlab-pygments==0.2.2 + # via nbconvert +jupyterlab-server==2.15.2 + # via + # jupyterlab + # retrolab +lxml==4.9.1 + # via nbconvert +mako==1.2.3 + # via alembic +markupsafe==2.1.1 + # via + # jinja2 + # mako + # nbconvert +matplotlib-inline==0.1.6 + # via + # ipykernel + # ipython +mistune==2.0.4 + # via nbconvert +nbclassic==0.4.4 + # via + # -r requirements.in + # jupyterlab + # retrolab +nbclient==0.6.8 + # via nbconvert +nbconvert==7.0.0 + # via + # jupyter-server + # nbclassic + # notebook +nbformat==5.6.1 + # via + # jupyter-server + # nbclassic + # nbclient + # nbconvert + # notebook +nbgitpuller==1.1.0 + # via -r requirements.in +nest-asyncio==1.5.6 + # via + # ipykernel + # jupyter-client + # nbclassic + # nbclient + # notebook +notebook==6.4.12 + # via + # jupyterlab + # nbgitpuller +notebook-shim==0.1.0 + # via nbclassic +oauthlib==3.2.1 + # via jupyterhub +packaging==21.3 + # via + # ipykernel + # jupyter-server + # jupyterhub + # jupyterlab + # jupyterlab-server + # nbconvert +pamela==1.0.0 + # via jupyterhub +pandocfilters==1.5.0 + # via nbconvert +parso==0.8.3 + # via jedi +pexpect==4.8.0 + # via ipython +pickleshare==0.7.5 + # via ipython +prometheus-client==0.14.1 + # via + # jupyter-server + # jupyterhub + # nbclassic + # notebook +prompt-toolkit==3.0.31 + # via ipython +psutil==5.9.2 + # via ipykernel +ptyprocess==0.7.0 + # via + # pexpect + # terminado +pure-eval==0.2.2 + # via stack-data +pycparser==2.21 + # via cffi +pygments==2.13.0 + # via + # ipython + # nbconvert +pyopenssl==22.1.0 + # via certipy +pyparsing==3.0.9 + # via packaging +pyrsistent==0.18.1 + # via jsonschema +python-dateutil==2.8.2 + # via + # jupyter-client + # jupyterhub +python-json-logger==2.0.4 + # via jupyter-telemetry +pytz==2022.4 + # via babel +pyzmq==24.0.1 + # via + # ipykernel + # jupyter-client + # jupyter-server + # nbclassic + # notebook +requests==2.28.1 + # via + # jupyterhub + # jupyterlab-server +retrolab==0.3.21 + # via -r requirements.in +ruamel-yaml==0.17.21 + # via jupyter-telemetry +ruamel-yaml-clib==0.2.6 + # via ruamel-yaml +send2trash==1.8.0 + # via + # jupyter-server + # nbclassic + # notebook +six==1.16.0 + # via + # asttokens + # bleach + # python-dateutil +sniffio==1.3.0 + # via anyio +soupsieve==2.3.2.post1 + # via beautifulsoup4 +sqlalchemy==1.4.41 + # via + # alembic + # jupyterhub +stack-data==0.5.1 + # via ipython +terminado==0.16.0 + # via + # jupyter-server + # nbclassic + # notebook +tinycss2==1.1.1 + # via nbconvert +tomli==2.0.1 + # via jupyterlab +tornado==6.2 + # via + # ipykernel + # jupyter-client + # jupyter-server + # jupyterhub + # jupyterlab + # nbclassic + # nbgitpuller + # notebook + # retrolab + # terminado +traitlets==5.4.0 + # via + # ipykernel + # ipython + # jupyter-client + # jupyter-core + # jupyter-server + # jupyter-telemetry + # jupyterhub + # matplotlib-inline + # nbclassic + # nbclient + # nbconvert + # nbformat + # notebook +urllib3==1.26.12 + # via requests +wcwidth==0.2.5 + # via prompt-toolkit +webencodings==0.5.1 + # via + # bleach + # tinycss2 +websocket-client==1.4.1 + # via jupyter-server +zipp==3.8.1 + # via importlib-metadata diff --git a/jupyterhub_V1/singleuser-nvidia/start-notebook.sh b/jupyterhub_V1/singleuser-nvidia/start-notebook.sh new file mode 100644 index 0000000..cfbdffd --- /dev/null +++ b/jupyterhub_V1/singleuser-nvidia/start-notebook.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +set -e + +# The Jupyter command to launch +# JupyterLab by default +DOCKER_STACKS_JUPYTER_CMD="${DOCKER_STACKS_JUPYTER_CMD:=lab}" + +if [[ -n "${JUPYTERHUB_API_TOKEN}" ]]; then + echo "WARNING: using start-singleuser.sh instead of start-notebook.sh to start a server associated with JupyterHub." + exec /usr/local/bin/start-singleuser.sh "$@" +fi + +wrapper="" +if [[ "${RESTARTABLE}" == "yes" ]]; then + wrapper="run-one-constantly" +fi + +# shellcheck disable=SC1091,SC2086 +exec /usr/local/bin/start.sh ${wrapper} jupyter ${DOCKER_STACKS_JUPYTER_CMD} ${NOTEBOOK_ARGS} "$@" \ No newline at end of file diff --git a/jupyterhub_V1/singleuser-nvidia/start-singleuser.sh b/jupyterhub_V1/singleuser-nvidia/start-singleuser.sh new file mode 100644 index 0000000..7c49847 --- /dev/null +++ b/jupyterhub_V1/singleuser-nvidia/start-singleuser.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +set -e + +# set default ip to 0.0.0.0 +if [[ "${NOTEBOOK_ARGS} $*" != *"--ip="* ]]; then + NOTEBOOK_ARGS="--ip=0.0.0.0 ${NOTEBOOK_ARGS}" +fi + +# shellcheck disable=SC1091,SC2086 +. /usr/local/bin/start.sh jupyterhub-singleuser ${NOTEBOOK_ARGS} "$@" \ No newline at end of file diff --git a/jupyterhub_V1/singleuser-nvidia/start.sh b/jupyterhub_V1/singleuser-nvidia/start.sh new file mode 100644 index 0000000..fef6b61 --- /dev/null +++ b/jupyterhub_V1/singleuser-nvidia/start.sh @@ -0,0 +1,262 @@ +#!/bin/bash +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +set -e + +# The _log function is used for everything this script wants to log. It will +# always log errors and warnings, but can be silenced for other messages +# by setting JUPYTER_DOCKER_STACKS_QUIET environment variable. +_log () { + if [[ "$*" == "ERROR:"* ]] || [[ "$*" == "WARNING:"* ]] || [[ "${JUPYTER_DOCKER_STACKS_QUIET}" == "" ]]; then + echo "$@" + fi +} +_log "Entered start.sh with args:" "$@" + +# The run-hooks function looks for .sh scripts to source and executable files to +# run within a passed directory. +run-hooks () { + if [[ ! -d "${1}" ]] ; then + return + fi + _log "${0}: running hooks in ${1} as uid / gid: $(id -u) / $(id -g)" + for f in "${1}/"*; do + case "${f}" in + *.sh) + _log "${0}: running script ${f}" + # shellcheck disable=SC1090 + source "${f}" + ;; + *) + if [[ -x "${f}" ]] ; then + _log "${0}: running executable ${f}" + "${f}" + else + _log "${0}: ignoring non-executable ${f}" + fi + ;; + esac + done + _log "${0}: done running hooks in ${1}" +} + +# A helper function to unset env vars listed in the value of the env var +# JUPYTER_ENV_VARS_TO_UNSET. +unset_explicit_env_vars () { + if [ -n "${JUPYTER_ENV_VARS_TO_UNSET}" ]; then + for env_var_to_unset in $(echo "${JUPYTER_ENV_VARS_TO_UNSET}" | tr ',' ' '); do + echo "Unset ${env_var_to_unset} due to JUPYTER_ENV_VARS_TO_UNSET" + unset "${env_var_to_unset}" + done + unset JUPYTER_ENV_VARS_TO_UNSET + fi +} + + +# Default to starting bash if no command was specified +if [ $# -eq 0 ]; then + cmd=( "bash" ) +else + cmd=( "$@" ) +fi + +# NOTE: This hook will run as the user the container was started with! +run-hooks /usr/local/bin/start-notebook.d + +# If the container started as the root user, then we have permission to refit +# the jovyan user, and ensure file permissions, grant sudo rights, and such +# things before we run the command passed to start.sh as the desired user +# (NB_USER). +# +if [ "$(id -u)" == 0 ] ; then + # Environment variables: + # - NB_USER: the desired username and associated home folder + # - NB_UID: the desired user id + # - NB_GID: a group id we want our user to belong to + # - NB_GROUP: a group name we want for the group + # - GRANT_SUDO: a boolean ("1" or "yes") to grant the user sudo rights + # - CHOWN_HOME: a boolean ("1" or "yes") to chown the user's home folder + # - CHOWN_EXTRA: a comma separated list of paths to chown + # - CHOWN_HOME_OPTS / CHOWN_EXTRA_OPTS: arguments to the chown commands + + # Refit the jovyan user to the desired the user (NB_USER) + if id jovyan &> /dev/null ; then + if ! usermod --home "/home/${NB_USER}" --login "${NB_USER}" jovyan 2>&1 | grep "no changes" > /dev/null; then + _log "Updated the jovyan user:" + _log "- username: jovyan -> ${NB_USER}" + _log "- home dir: /home/jovyan -> /home/${NB_USER}" + fi + elif ! id -u "${NB_USER}" &> /dev/null; then + _log "ERROR: Neither the jovyan user or '${NB_USER}' exists. This could be the result of stopping and starting, the container with a different NB_USER environment variable." + exit 1 + fi + # Ensure the desired user (NB_USER) gets its desired user id (NB_UID) and is + # a member of the desired group (NB_GROUP, NB_GID) + if [ "${NB_UID}" != "$(id -u "${NB_USER}")" ] || [ "${NB_GID}" != "$(id -g "${NB_USER}")" ]; then + _log "Update ${NB_USER}'s UID:GID to ${NB_UID}:${NB_GID}" + # Ensure the desired group's existence + if [ "${NB_GID}" != "$(id -g "${NB_USER}")" ]; then + groupadd --force --gid "${NB_GID}" --non-unique "${NB_GROUP:-${NB_USER}}" + fi + # Recreate the desired user as we want it + userdel "${NB_USER}" + useradd --home "/home/${NB_USER}" --uid "${NB_UID}" --gid "${NB_GID}" --groups 100 --no-log-init "${NB_USER}" + fi + + # Move or symlink the jovyan home directory to the desired users home + # directory if it doesn't already exist, and update the current working + # directory to the new location if needed. + if [[ "${NB_USER}" != "jovyan" ]]; then + if [[ ! -e "/home/${NB_USER}" ]]; then + _log "Attempting to copy /home/jovyan to /home/${NB_USER}..." + mkdir "/home/${NB_USER}" + if cp -a /home/jovyan/. "/home/${NB_USER}/"; then + _log "Success!" + else + _log "Failed to copy data from /home/jovyan to /home/${NB_USER}!" + _log "Attempting to symlink /home/jovyan to /home/${NB_USER}..." + if ln -s /home/jovyan "/home/${NB_USER}"; then + _log "Success creating symlink!" + else + _log "ERROR: Failed copy data from /home/jovyan to /home/${NB_USER} or to create symlink!" + exit 1 + fi + fi + fi + # Ensure the current working directory is updated to the new path + if [[ "${PWD}/" == "/home/jovyan/"* ]]; then + new_wd="/home/${NB_USER}/${PWD:13}" + _log "Changing working directory to ${new_wd}" + cd "${new_wd}" + fi + fi + + # Optionally ensure the desired user get filesystem ownership of it's home + # folder and/or additional folders + if [[ "${CHOWN_HOME}" == "1" || "${CHOWN_HOME}" == "yes" ]]; then + _log "Ensuring /home/${NB_USER} is owned by ${NB_UID}:${NB_GID} ${CHOWN_HOME_OPTS:+(chown options: ${CHOWN_HOME_OPTS})}" + # shellcheck disable=SC2086 + chown ${CHOWN_HOME_OPTS} "${NB_UID}:${NB_GID}" "/home/${NB_USER}" + fi + if [ -n "${CHOWN_EXTRA}" ]; then + for extra_dir in $(echo "${CHOWN_EXTRA}" | tr ',' ' '); do + _log "Ensuring ${extra_dir} is owned by ${NB_UID}:${NB_GID} ${CHOWN_EXTRA_OPTS:+(chown options: ${CHOWN_EXTRA_OPTS})}" + # shellcheck disable=SC2086 + chown ${CHOWN_EXTRA_OPTS} "${NB_UID}:${NB_GID}" "${extra_dir}" + done + fi + + # Update potentially outdated environment variables since image build + export XDG_CACHE_HOME="/home/${NB_USER}/.cache" + + # Prepend ${CONDA_DIR}/bin to sudo secure_path + sed -r "s#Defaults\s+secure_path\s*=\s*\"?([^\"]+)\"?#Defaults secure_path=\"${CONDA_DIR}/bin:\1\"#" /etc/sudoers | grep secure_path > /etc/sudoers.d/path + + # Optionally grant passwordless sudo rights for the desired user + if [[ "$GRANT_SUDO" == "1" || "$GRANT_SUDO" == "yes" ]]; then + _log "Granting ${NB_USER} passwordless sudo rights!" + echo "${NB_USER} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/added-by-start-script + fi + + # NOTE: This hook is run as the root user! + run-hooks /usr/local/bin/before-notebook.d + + unset_explicit_env_vars + _log "Running as ${NB_USER}:" "${cmd[@]}" + exec sudo --preserve-env --set-home --user "${NB_USER}" \ + PATH="${PATH}" \ + PYTHONPATH="${PYTHONPATH:-}" \ + "${cmd[@]}" + # Notes on how we ensure that the environment that this container is started + # with is preserved (except vars listed in JUPYTER_ENV_VARS_TO_UNSET) when + # we transition from running as root to running as NB_USER. + # + # - We use `sudo` to execute the command as NB_USER. What then + # happens to the environment will be determined by configuration in + # /etc/sudoers and /etc/sudoers.d/* as well as flags we pass to the sudo + # command. The behavior can be inspected with `sudo -V` run as root. + # + # ref: `man sudo` https://linux.die.net/man/8/sudo + # ref: `man sudoers` https://www.sudo.ws/man/1.8.15/sudoers.man.html + # + # - We use the `--preserve-env` flag to pass through most environment + # variables, but understand that exceptions are caused by the sudoers + # configuration: `env_delete` and `env_check`. + # + # - We use the `--set-home` flag to set the HOME variable appropriately. + # + # - To reduce the default list of variables deleted by sudo, we could have + # used `env_delete` from /etc/sudoers. It has higher priority than the + # `--preserve-env` flag and the `env_keep` configuration. + # + # - We preserve PATH and PYTHONPATH explicitly. Note however that sudo + # resolves `${cmd[@]}` using the "secure_path" variable we modified + # above in /etc/sudoers.d/path. Thus PATH is irrelevant to how the above + # sudo command resolves the path of `${cmd[@]}`. The PATH will be relevant + # for resolving paths of any subprocesses spawned by `${cmd[@]}`. + +# The container didn't start as the root user, so we will have to act as the +# user we started as. +else + # Warn about misconfiguration of: granting sudo rights + if [[ "${GRANT_SUDO}" == "1" || "${GRANT_SUDO}" == "yes" ]]; then + _log "WARNING: container must be started as root to grant sudo permissions!" + fi + + JOVYAN_UID="$(id -u jovyan 2>/dev/null)" # The default UID for the jovyan user + JOVYAN_GID="$(id -g jovyan 2>/dev/null)" # The default GID for the jovyan user + + # Attempt to ensure the user uid we currently run as has a named entry in + # the /etc/passwd file, as it avoids software crashing on hard assumptions + # on such entry. Writing to the /etc/passwd was allowed for the root group + # from the Dockerfile during build. + # + # ref: https://github.com/jupyter/docker-stacks/issues/552 + if ! whoami &> /dev/null; then + _log "There is no entry in /etc/passwd for our UID=$(id -u). Attempting to fix..." + if [[ -w /etc/passwd ]]; then + _log "Renaming old jovyan user to nayvoj ($(id -u jovyan):$(id -g jovyan))" + + # We cannot use "sed --in-place" since sed tries to create a temp file in + # /etc/ and we may not have write access. Apply sed on our own temp file: + sed --expression="s/^jovyan:/nayvoj:/" /etc/passwd > /tmp/passwd + echo "${NB_USER}:x:$(id -u):$(id -g):,,,:/home/jovyan:/bin/bash" >> /tmp/passwd + cat /tmp/passwd > /etc/passwd + rm /tmp/passwd + + _log "Added new ${NB_USER} user ($(id -u):$(id -g)). Fixed UID!" + + if [[ "${NB_USER}" != "jovyan" ]]; then + _log "WARNING: user is ${NB_USER} but home is /home/jovyan. You must run as root to rename the home directory!" + fi + else + _log "WARNING: unable to fix missing /etc/passwd entry because we don't have write permission. Try setting gid=0 with \"--user=$(id -u):0\"." + fi + fi + + # Warn about misconfiguration of: desired username, user id, or group id. + # A misconfiguration occurs when the user modifies the default values of + # NB_USER, NB_UID, or NB_GID, but we cannot update those values because we + # are not root. + if [[ "${NB_USER}" != "jovyan" && "${NB_USER}" != "$(id -un)" ]]; then + _log "WARNING: container must be started as root to change the desired user's name with NB_USER=\"${NB_USER}\"!" + fi + if [[ "${NB_UID}" != "${JOVYAN_UID}" && "${NB_UID}" != "$(id -u)" ]]; then + _log "WARNING: container must be started as root to change the desired user's id with NB_UID=\"${NB_UID}\"!" + fi + if [[ "${NB_GID}" != "${JOVYAN_GID}" && "${NB_GID}" != "$(id -g)" ]]; then + _log "WARNING: container must be started as root to change the desired user's group id with NB_GID=\"${NB_GID}\"!" + fi + + # Warn if the user isn't able to write files to ${HOME} + if [[ ! -w /home/jovyan ]]; then + _log "WARNING: no write access to /home/jovyan. Try starting the container with group 'users' (100), e.g. using \"--group-add=users\"." + fi + + # NOTE: This hook is run as the user we started the container as! + run-hooks /usr/local/bin/before-notebook.d + unset_explicit_env_vars + _log "Executing the command:" "${cmd[@]}" + exec "${cmd[@]}" +fi \ No newline at end of file