mirror of
https://github.com/jupyter/docker-stacks.git
synced 2025-10-14 21:42:57 +00:00
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:
@@ -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
|
||||||
|
@@ -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
|
||||||
|
@@ -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
|
||||||
|
|
||||||
|
38
images/docker-stacks-foundation/run-hooks.sh
Executable file
38
images/docker-stacks-foundation/run-hooks.sh
Executable 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}"
|
@@ -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[@]}"
|
||||||
|
@@ -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
|
||||||
|
5
tests/docker-stacks-foundation/run-hooks-data/executable.py
Executable file
5
tests/docker-stacks-foundation/run-hooks-data/executable.py
Executable 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")
|
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Copyright (c) Jupyter Development Team.
|
||||||
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
|
||||||
|
assert False
|
5
tests/docker-stacks-foundation/run-hooks-data/run-me.sh
Normal file
5
tests/docker-stacks-foundation/run-hooks-data/run-me.sh
Normal 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
|
95
tests/docker-stacks-foundation/test_run_hooks.py
Normal file
95
tests/docker-stacks-foundation/test_run_hooks.py
Normal 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
|
Reference in New Issue
Block a user