Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement run-hooks as a separate script #1979

Merged
merged 4 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/using/common.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ You do so by passing arguments to the `docker run` command.

```{note}
`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`.
If you need to set a `umask` for these, you **must** set the `umask` value for each command.
```
Expand Down Expand Up @@ -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
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.

## SSL Certificates
Expand Down
1 change: 1 addition & 0 deletions docs/using/selecting.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ It contains:
with ownership over the `/home/jovyan` and `/opt/conda` paths
- `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 `run-hooks.sh` script, which can source/run files in a given directory
- Options for a passwordless sudo
- Common system libraries like `bzip2`, `ca-certificates`, `locales`
- `wget` to download external files
Expand Down
2 changes: 1 addition & 1 deletion images/docker-stacks-foundation/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ ENTRYPOINT ["tini", "-g", "--"]
CMD ["start.sh"]

# 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

Expand Down
38 changes: 38 additions & 0 deletions images/docker-stacks-foundation/run-hooks.sh
Original file line number Diff line number Diff line change
@@ -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}"
36 changes: 6 additions & 30 deletions images/docker-stacks-foundation/start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,33 +14,6 @@ _log () {
}
_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
# JUPYTER_ENV_VARS_TO_UNSET.
unset_explicit_env_vars () {
Expand All @@ -62,7 +35,8 @@ else
fi

# 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
# the jovyan user, and ensure file permissions, grant sudo rights, and such
Expand Down Expand Up @@ -160,7 +134,8 @@ if [ "$(id -u)" == 0 ] ; then
fi

# 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
_log "Running as ${NB_USER}:" "${cmd[@]}"
Expand Down Expand Up @@ -255,7 +230,8 @@ else
fi

# 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
_log "Executing the command:" "${cmd[@]}"
exec "${cmd[@]}"
Expand Down
6 changes: 5 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ def run_and_wait(
timeout: int,
no_warnings: bool = True,
no_errors: bool = True,
no_failure: bool = True,
**kwargs: Any,
) -> str:
running_container = self.run_detached(**kwargs)
Expand All @@ -119,7 +120,10 @@ def run_and_wait(
assert not self.get_warnings(logs)
if no_errors:
assert not self.get_errors(logs)
assert rv == 0 or rv["StatusCode"] == 0
if no_failure:
assert rv == 0 or rv["StatusCode"] == 0
else:
assert rv != 0 and rv["StatusCode"] != 0
return logs

@staticmethod
Expand Down
5 changes: 5 additions & 0 deletions tests/docker-stacks-foundation/run-hooks-data/executable.py
Original file line number Diff line number Diff line change
@@ -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")
Original file line number Diff line number Diff line change
@@ -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 changes: 5 additions & 0 deletions tests/docker-stacks-foundation/run-hooks-data/run-me.sh
Original file line number Diff line number Diff line change
@@ -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 changes: 95 additions & 0 deletions tests/docker-stacks-foundation/test_run_hooks.py
Original file line number Diff line number Diff line change
@@ -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