Implement run-hooks as a separate script (#1979)

* Implement run-hooks as a separate script

* Add more tests

* Add more docs
This commit is contained in:
Ayaz Salikhov
2023-08-25 01:53:00 +04:00
committed by GitHub
parent c2bf3c6bfd
commit 74bbd0bffc
10 changed files with 163 additions and 34 deletions

View File

@@ -86,7 +86,7 @@ You do so by passing arguments to the `docker run` command.
```{note} ```{note}
`NB_UMASK` when set only applies to the Jupyter process itself - `NB_UMASK` when set only applies to the Jupyter process itself -
you cannot use it to set a `umask` for additional files created during run-hooks. you cannot use it to set a `umask` for additional files created during `run-hooks.sh`.
For example, via `pip` or `conda`. For example, via `pip` or `conda`.
If you need to set a `umask` for these, you **must** set the `umask` value for each command. If you need to set a `umask` for these, you **must** set the `umask` value for each command.
``` ```
@@ -135,7 +135,7 @@ or executables (`chmod +x`) to be run to the paths below:
- `/usr/local/bin/before-notebook.d/` - handled **after** all the standard options noted above are applied - `/usr/local/bin/before-notebook.d/` - handled **after** all the standard options noted above are applied
and ran right before the Server launches and ran right before the Server launches
See the `run-hooks` function in the [`jupyter/docker-stacks-foundation start.sh`](https://github.com/jupyter/docker-stacks/blob/main/images/docker-stacks-foundation/start.sh) See the `run-hooks.sh` script [here](https://github.com/jupyter/docker-stacks/blob/main/images/docker-stacks-foundation/run-hooks.sh) and how it's used in the [`start.sh`](https://github.com/jupyter/docker-stacks/blob/main/images/docker-stacks-foundation/start.sh)
script for execution details. script for execution details.
## SSL Certificates ## SSL Certificates

View File

@@ -36,6 +36,7 @@ It contains:
with ownership over the `/home/jovyan` and `/opt/conda` paths with ownership over the `/home/jovyan` and `/opt/conda` paths
- `tini` as the container entry point - `tini` as the container entry point
- A `start.sh` script as the default command - useful for running alternative commands in the container as applications are added (e.g. `ipython`, `jupyter kernelgateway`, `jupyter lab`) - A `start.sh` script as the default command - useful for running alternative commands in the container as applications are added (e.g. `ipython`, `jupyter kernelgateway`, `jupyter lab`)
- A `run-hooks.sh` script, which can source/run files in a given directory
- Options for a passwordless sudo - Options for a passwordless sudo
- Common system libraries like `bzip2`, `ca-certificates`, `locales` - Common system libraries like `bzip2`, `ca-certificates`, `locales`
- `wget` to download external files - `wget` to download external files

View File

@@ -127,7 +127,7 @@ ENTRYPOINT ["tini", "-g", "--"]
CMD ["start.sh"] CMD ["start.sh"]
# Copy local files as late as possible to avoid cache busting # Copy local files as late as possible to avoid cache busting
COPY start.sh /usr/local/bin/ COPY run-hooks.sh start.sh /usr/local/bin/
USER root USER root

View File

@@ -0,0 +1,38 @@
#!/bin/bash
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
# The run-hooks.sh script looks for *.sh scripts to source
# and executable files to run within a passed directory
if [ "$#" -ne 1 ]; then
echo "Should pass exactly one directory"
return 1
fi
if [[ ! -d "${1}" ]] ; then
echo "Directory ${1} doesn't exist or is not a directory"
return 1
fi
echo "Running hooks in: ${1} as uid: $(id -u) gid: $(id -g)"
for f in "${1}/"*; do
# Hadling a case when the directory is empty
[ -e "${f}" ] || continue
case "${f}" in
*.sh)
echo "Sourcing shell script: ${f}"
# shellcheck disable=SC1090
source "${f}"
;;
*)
if [ -x "${f}" ] ; then
echo "Running executable: ${f}"
"${f}"
else
echo "Ignoring non-executable: ${f}"
fi
;;
esac
done
echo "Done running hooks in: ${1}"

View File

@@ -14,33 +14,6 @@ _log () {
} }
_log "Entered start.sh with args:" "$@" _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: $(id -u) gid: $(id -g)"
for f in "${1}/"*; do
case "${f}" in
*.sh)
_log "${0}: sourcing shell 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 # A helper function to unset env vars listed in the value of the env var
# JUPYTER_ENV_VARS_TO_UNSET. # JUPYTER_ENV_VARS_TO_UNSET.
unset_explicit_env_vars () { unset_explicit_env_vars () {
@@ -62,7 +35,8 @@ else
fi fi
# NOTE: This hook will run as the user the container was started with! # NOTE: This hook will run as the user the container was started with!
run-hooks /usr/local/bin/start-notebook.d # shellcheck disable=SC1091
source /usr/local/bin/run-hooks.sh /usr/local/bin/start-notebook.d
# If the container started as the root user, then we have permission to refit # 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 # the jovyan user, and ensure file permissions, grant sudo rights, and such
@@ -160,7 +134,8 @@ if [ "$(id -u)" == 0 ] ; then
fi fi
# NOTE: This hook is run as the root user! # NOTE: This hook is run as the root user!
run-hooks /usr/local/bin/before-notebook.d # shellcheck disable=SC1091
source /usr/local/bin/run-hooks.sh /usr/local/bin/before-notebook.d
unset_explicit_env_vars unset_explicit_env_vars
_log "Running as ${NB_USER}:" "${cmd[@]}" _log "Running as ${NB_USER}:" "${cmd[@]}"
@@ -255,7 +230,8 @@ else
fi fi
# NOTE: This hook is run as the user we started the container as! # NOTE: This hook is run as the user we started the container as!
run-hooks /usr/local/bin/before-notebook.d # shellcheck disable=SC1091
source /usr/local/bin/run-hooks.sh /usr/local/bin/before-notebook.d
unset_explicit_env_vars unset_explicit_env_vars
_log "Executing the command:" "${cmd[@]}" _log "Executing the command:" "${cmd[@]}"
exec "${cmd[@]}" exec "${cmd[@]}"

View File

@@ -108,6 +108,7 @@ class TrackedContainer:
timeout: int, timeout: int,
no_warnings: bool = True, no_warnings: bool = True,
no_errors: bool = True, no_errors: bool = True,
no_failure: bool = True,
**kwargs: Any, **kwargs: Any,
) -> str: ) -> str:
running_container = self.run_detached(**kwargs) running_container = self.run_detached(**kwargs)
@@ -119,7 +120,10 @@ class TrackedContainer:
assert not self.get_warnings(logs) assert not self.get_warnings(logs)
if no_errors: if no_errors:
assert not self.get_errors(logs) assert not self.get_errors(logs)
if no_failure:
assert rv == 0 or rv["StatusCode"] == 0 assert rv == 0 or rv["StatusCode"] == 0
else:
assert rv != 0 and rv["StatusCode"] != 0
return logs return logs
@staticmethod @staticmethod

View File

@@ -0,0 +1,5 @@
#!/usr/bin/env python3
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
print("Executable python file was successfully run")

View File

@@ -0,0 +1,5 @@
#!/usr/bin/env python3
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
assert False

View File

@@ -0,0 +1,5 @@
#!/bin/bash
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
export SOME_VAR=123

View File

@@ -0,0 +1,95 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import logging
from pathlib import Path
from tests.conftest import TrackedContainer
LOGGER = logging.getLogger(__name__)
THIS_DIR = Path(__file__).parent.resolve()
def test_run_hooks_zero_args(container: TrackedContainer) -> None:
logs = container.run_and_wait(
timeout=5,
tty=True,
no_failure=False,
command=["bash", "-c", "source /usr/local/bin/run-hooks.sh"],
)
assert "Should pass exactly one directory" in logs
def test_run_hooks_two_args(container: TrackedContainer) -> None:
logs = container.run_and_wait(
timeout=5,
tty=True,
no_failure=False,
command=[
"bash",
"-c",
"source /usr/local/bin/run-hooks.sh first-arg second-arg",
],
)
assert "Should pass exactly one directory" in logs
def test_run_hooks_missing_dir(container: TrackedContainer) -> None:
logs = container.run_and_wait(
timeout=5,
tty=True,
no_failure=False,
command=[
"bash",
"-c",
"source /usr/local/bin/run-hooks.sh /tmp/missing-dir/",
],
)
assert "Directory /tmp/missing-dir/ doesn't exist or is not a directory" in logs
def test_run_hooks_dir_is_file(container: TrackedContainer) -> None:
logs = container.run_and_wait(
timeout=5,
tty=True,
no_failure=False,
command=[
"bash",
"-c",
"touch /tmp/some-file && source /usr/local/bin/run-hooks.sh /tmp/some-file",
],
)
assert "Directory /tmp/some-file doesn't exist or is not a directory" in logs
def test_run_hooks_empty_dir(container: TrackedContainer) -> None:
container.run_and_wait(
timeout=5,
tty=True,
command=[
"bash",
"-c",
"mkdir /tmp/empty-dir && source /usr/local/bin/run-hooks.sh /tmp/empty-dir/",
],
)
def test_run_hooks_with_files(container: TrackedContainer) -> None:
host_data_dir = THIS_DIR / "run-hooks-data"
cont_data_dir = "/home/jovyan/data"
# https://forums.docker.com/t/all-files-appear-as-executable-in-file-paths-using-bind-mount/99921
# Unfortunately, Docker treats all files in mounter dir as executable files
# So we make a copy of mounted dir inside a container
command = (
"cp -r /home/jovyan/data/ /home/jovyan/data-copy/ &&"
"source /usr/local/bin/run-hooks.sh /home/jovyan/data-copy/ &&"
"echo SOME_VAR is ${SOME_VAR}"
)
logs = container.run_and_wait(
timeout=5,
volumes={str(host_data_dir): {"bind": cont_data_dir, "mode": "ro"}},
tty=True,
command=["bash", "-c", command],
)
assert "Executable python file was successfully" in logs
assert "Ignoring non-executable: /home/jovyan/data-copy//non_executable.py" in logs
assert "SOME_VAR is 123" in logs