Skip to content

Commit 6e437aa

Browse files
authored
Make start.sh the entrypoint (#2087)
1 parent b71f4cb commit 6e437aa

File tree

21 files changed

+86
-58
lines changed

21 files changed

+86
-58
lines changed

docs/using/common.md

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -225,21 +225,17 @@ docker run -it --rm \
225225

226226
### `start.sh`
227227

228-
The `start-notebook.py` script inherits most of its option handling capability from a more generic `start.sh` script.
229-
The `start.sh` script supports all the features described above but allows you to specify an arbitrary command to execute.
228+
Most of the configuration options in the `start-notebook.py` script are handled by an internal `start.sh` script that automatically runs before the command provided to the container
229+
(it's set as the container entrypoint).
230+
This allows you to specify an arbitrary command that takes advantage of all these features.
230231
For example, to run the text-based `ipython` console in a container, do the following:
231232

232233
```bash
233-
docker run -it --rm quay.io/jupyter/base-notebook start.sh ipython
234+
docker run -it --rm quay.io/jupyter/base-notebook ipython
234235
```
235236

236237
This script is handy when you derive a new Dockerfile from this image and install additional Jupyter applications with subcommands like `jupyter console`, `jupyter kernelgateway`, etc.
237238

238-
### Others
239-
240-
You can bypass the provided scripts and specify an arbitrary start command.
241-
If you do, keep in mind that features, supported by the `start.sh` script and its kin, will not function (e.g., `GRANT_SUDO`).
242-
243239
## Conda Environments
244240

245241
The default Python 3.x [Conda environment](https://conda.io/projects/conda/en/latest/user-guide/concepts/environments.html) resides in `/opt/conda`.

docs/using/selecting.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ It contains:
3434
- [mamba](https://github.com/mamba-org/mamba): "reimplementation of the conda package manager in C++". We use this package manager by default when installing packages.
3535
- Unprivileged user `jovyan` (`uid=1000`, configurable, [see options in the common features section](./common.md) of this documentation) in group `users` (`gid=100`)
3636
with ownership over the `/home/jovyan` and `/opt/conda` paths
37-
- `tini` as the container entry point
38-
- 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`)
37+
- `tini` and a `start.sh` script as the container entry point - useful for running alternative commands in the container as applications are added (e.g. `ipython`, `jupyter kernelgateway`, `jupyter lab`)
3938
- A `run-hooks.sh` script, which can source/run files in a given directory
4039
- Options for a passwordless sudo
4140
- Common system libraries like `bzip2`, `ca-certificates`, `locales`

images/base-notebook/start-notebook.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@
1414
os.execvp(command[0], command)
1515

1616

17-
# Wrap everything in start.sh, no matter what
18-
command = ["/usr/local/bin/start.sh"]
17+
# Entrypoint is start.sh
18+
command = []
1919

2020
# If we want to survive restarts, tell that to start.sh
2121
if os.environ.get("RESTARTABLE") == "yes":
@@ -40,4 +40,5 @@
4040
command += sys.argv[1:]
4141

4242
# Execute the command!
43+
print("Executing: " + " ".join(command))
4344
os.execvp(command[0], command)

images/base-notebook/start-singleuser.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
import shlex
66
import sys
77

8-
command = ["/usr/local/bin/start.sh", "jupyterhub-singleuser"]
8+
# Entrypoint is start.sh
9+
command = ["jupyterhub-singleuser"]
910

1011
# set default ip to 0.0.0.0
1112
if "--ip=" not in os.environ.get("NOTEBOOK_ARGS", ""):
@@ -20,4 +21,5 @@
2021
command += sys.argv[1:]
2122

2223
# Execute the command!
24+
print("Executing: " + " ".join(command))
2325
os.execvp(command[0], command)

images/docker-stacks-foundation/Dockerfile

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,8 +124,7 @@ RUN set -x && \
124124
fix-permissions "/home/${NB_USER}"
125125

126126
# Configure container startup
127-
ENTRYPOINT ["tini", "-g", "--"]
128-
CMD ["start.sh"]
127+
ENTRYPOINT ["tini", "-g", "--", "start.sh"]
129128

130129
# Copy local files as late as possible to avoid cache busting
131130
COPY run-hooks.sh start.sh /usr/local/bin/

images/docker-stacks-foundation/start.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ else
3434
cmd=( "$@" )
3535
fi
3636

37+
# Backwards compatibility: `start.sh` is executed by default in ENTRYPOINT
38+
# so it should no longer be specified in CMD
39+
if [ "${_START_SH_EXECUTED}" = "1" ]; then
40+
_log "WARNING: start.sh is the default ENTRYPOINT, do not include it in CMD"
41+
_log "Executing the command:" "${cmd[@]}"
42+
exec "${cmd[@]}"
43+
else
44+
export _START_SH_EXECUTED=1
45+
fi
46+
47+
3748
# NOTE: This hook will run as the user the container was started with!
3849
# shellcheck disable=SC1091
3950
source /usr/local/bin/run-hooks.sh /usr/local/bin/start-notebook.d

tests/R_mimetype_check.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,14 @@ def check_r_mimetypes(container: TrackedContainer) -> None:
1111
"""Check if Rscript command can be executed"""
1212
LOGGER.info("Test that R command can be executed ...")
1313
R_MIMETYPES_CHECK_CMD = 'if (length(getOption("jupyter.plot_mimetypes")) != 5) {stop("missing jupyter.plot_mimetypes")}'
14+
command = ["Rscript", "-e", R_MIMETYPES_CHECK_CMD]
1415
logs = container.run_and_wait(
1516
timeout=10,
1617
tty=True,
17-
command=["Rscript", "-e", R_MIMETYPES_CHECK_CMD],
18+
command=command,
1819
)
1920
LOGGER.debug(f"{logs=}")
20-
assert len(logs) == 0, f"Command {R_MIMETYPES_CHECK_CMD=} failed"
21+
# If there is any output after this it means there was an error
22+
assert logs.splitlines()[-1] == "Executing the command: " + " ".join(
23+
command
24+
), f"Command {R_MIMETYPES_CHECK_CMD=} failed"

tests/all-spark-notebook/test_spark_notebooks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def test_nbconvert(container: TrackedContainer, test_file: str) -> None:
3333
timeout=60,
3434
volumes={str(host_data_dir): {"bind": cont_data_dir, "mode": "ro"}},
3535
tty=True,
36-
command=["start.sh", "bash", "-c", command],
36+
command=["bash", "-c", command],
3737
)
3838

3939
expected_file = f"{output_dir}/{test_file}.md"

tests/base-notebook/test_container_options.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def test_nb_user_change(container: TrackedContainer) -> None:
3535
tty=True,
3636
user="root",
3737
environment=[f"NB_USER={nb_user}", "CHOWN_HOME=yes"],
38-
command=["start.sh", "bash", "-c", "sleep infinity"],
38+
command=["bash", "-c", "sleep infinity"],
3939
)
4040

4141
# Give the chown time to complete.

tests/base-notebook/test_pandoc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,6 @@ def test_pandoc(container: TrackedContainer) -> None:
1212
logs = container.run_and_wait(
1313
timeout=10,
1414
tty=True,
15-
command=["start.sh", "bash", "-c", 'echo "**BOLD**" | pandoc'],
15+
command=["bash", "-c", 'echo "**BOLD**" | pandoc'],
1616
)
1717
assert "<p><strong>BOLD</strong></p>" in logs

tests/base-notebook/test_start_container.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ def test_start_notebook(
5353
LOGGER.debug(logs)
5454
# checking that the expected command is launched
5555
assert (
56-
f"Executing the command: {expected_command}" in logs
56+
f"Executing: {expected_command}" in logs
5757
), f"Not the expected command ({expected_command}) was launched"
5858
# checking errors and warnings in logs
5959
assert "ERROR" not in logs, "ERROR(s) found in logs"
@@ -76,10 +76,7 @@ def test_tini_entrypoint(
7676
https://superuser.com/questions/632979/if-i-know-the-pid-number-of-a-process-how-can-i-get-its-name
7777
"""
7878
LOGGER.info(f"Test that {command} is launched as PID {pid} ...")
79-
running_container = container.run_detached(
80-
tty=True,
81-
command=["start.sh"],
82-
)
79+
running_container = container.run_detached(tty=True)
8380
# Select the PID 1 and get the corresponding command
8481
cmd = running_container.exec_run(f"ps -p {pid} -o comm=")
8582
output = cmd.output.decode("utf-8").strip("\n")

tests/docker-stacks-foundation/test_packages.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,20 @@
2121
2222
Example:
2323
24-
$ make test/base-notebook
24+
$ make test/docker-stacks-foundation
2525
2626
# [...]
27-
# tests/base-notebook/test_packages.py::test_python_packages
28-
# ---------------------------------------------------------------------------------------------- live log setup ----------------------------------------------------------------------------------------------
29-
# 2023-11-04 23:59:01 [ INFO] Starting container quay.io/jupyter/base-notebook ... (package_helper.py:55)
30-
# 2023-11-04 23:59:01 [ INFO] Running quay.io/jupyter/base-notebook with args {'detach': True, 'tty': True, 'command': ['start.sh', 'bash', '-c', 'sleep infinity']} ... (conftest.py:99)
31-
# 2023-11-04 23:59:01 [ INFO] Grabbing the list of manually requested packages ... (package_helper.py:83)
32-
# ---------------------------------------------------------------------------------------------- live log call -----------------------------------------------------------------------------------------------
33-
# 2023-11-04 23:59:02 [ INFO] Testing the import of packages ... (test_packages.py:152)
34-
# 2023-11-04 23:59:02 [ INFO] Trying to import mamba (test_packages.py:154)
27+
# tests/docker-stacks-foundation/test_packages.py::test_python_packages
28+
# -------------------------------- live log setup --------------------------------
29+
# 2024-01-21 17:46:43 [ INFO] Starting container quay.io/jupyter/docker-stacks-foundation ... (package_helper.py:55)
30+
# 2024-01-21 17:46:43 [ INFO] Running quay.io/jupyter/docker-stacks-foundation with args {'detach': True, 'tty': True, 'command': ['bash', '-c', 'sleep infinity']} ... (conftest.py:99)
31+
# 2024-01-21 17:46:44 [ INFO] Grabbing the list of manually requested packages ... (package_helper.py:83)
32+
# -------------------------------- live log call ---------------------------------
33+
# 2024-01-21 17:46:44 [ INFO] Testing the import of packages ... (test_packages.py:151)
34+
# 2024-01-21 17:46:44 [ INFO] Trying to import mamba (test_packages.py:153)
35+
# 2024-01-21 17:46:44 [ INFO] Trying to import jupyter_core (test_packages.py:153)
36+
PASSED [ 17%]
37+
# ------------------------------ live log teardown -------------------------------
3538
# [...]
3639
3740
"""

tests/docker-stacks-foundation/test_python_version.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ def test_python_version(container: TrackedContainer) -> None:
1717
tty=True,
1818
command=["python", "--version"],
1919
)
20-
assert logs.startswith("Python ")
21-
full_version = logs.split()[1]
20+
python = next(line for line in logs.splitlines() if line.startswith("Python "))
21+
full_version = python.split()[1]
2222
major_minor_version = full_version[: full_version.rfind(".")]
2323

2424
assert major_minor_version == EXPECTED_PYTHON_VERSION
@@ -31,4 +31,4 @@ def test_python_pinned_version(container: TrackedContainer) -> None:
3131
tty=True,
3232
command=["cat", "/opt/conda/conda-meta/pinned"],
3333
)
34-
assert logs.startswith(f"python {EXPECTED_PYTHON_VERSION}.*")
34+
assert f"python {EXPECTED_PYTHON_VERSION}.*" in logs

tests/docker-stacks-foundation/test_units.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,5 +34,5 @@ def test_units(container: TrackedContainer) -> None:
3434
timeout=30,
3535
volumes={str(host_data_dir): {"bind": cont_data_dir, "mode": "ro"}},
3636
tty=True,
37-
command=["start.sh", "python", f"{cont_data_dir}/{test_file_name}"],
37+
command=["python", f"{cont_data_dir}/{test_file_name}"],
3838
)

tests/docker-stacks-foundation/test_user_options.py

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def test_uid_change(container: TrackedContainer) -> None:
1818
tty=True,
1919
user="root",
2020
environment=["NB_UID=1010"],
21-
command=["start.sh", "bash", "-c", "id && touch /opt/conda/test-file"],
21+
command=["bash", "-c", "id && touch /opt/conda/test-file"],
2222
)
2323
assert "uid=1010(jovyan)" in logs
2424

@@ -30,7 +30,7 @@ def test_gid_change(container: TrackedContainer) -> None:
3030
tty=True,
3131
user="root",
3232
environment=["NB_GID=110"],
33-
command=["start.sh", "id"],
33+
command=["id"],
3434
)
3535
assert "gid=110(jovyan)" in logs
3636
assert "groups=110(jovyan),100(users)" in logs
@@ -43,7 +43,7 @@ def test_nb_user_change(container: TrackedContainer) -> None:
4343
tty=True,
4444
user="root",
4545
environment=[f"NB_USER={nb_user}", "CHOWN_HOME=yes"],
46-
command=["start.sh", "bash", "-c", "sleep infinity"],
46+
command=["bash", "-c", "sleep infinity"],
4747
)
4848

4949
# Give the chown time to complete.
@@ -99,7 +99,6 @@ def test_chown_extra(container: TrackedContainer) -> None:
9999
"CHOWN_EXTRA_OPTS=-R",
100100
],
101101
command=[
102-
"start.sh",
103102
"bash",
104103
"-c",
105104
"stat -c '%n:%u:%g' /home/jovyan/.bashrc /opt/conda/bin/jupyter",
@@ -123,7 +122,7 @@ def test_chown_home(container: TrackedContainer) -> None:
123122
"NB_UID=1010",
124123
"NB_GID=101",
125124
],
126-
command=["start.sh", "bash", "-c", "stat -c '%n:%u:%g' /home/kitten/.bashrc"],
125+
command=["bash", "-c", "stat -c '%n:%u:%g' /home/kitten/.bashrc"],
127126
)
128127
assert "/home/kitten/.bashrc:1010:101" in logs
129128

@@ -135,7 +134,7 @@ def test_sudo(container: TrackedContainer) -> None:
135134
tty=True,
136135
user="root",
137136
environment=["GRANT_SUDO=yes"],
138-
command=["start.sh", "sudo", "id"],
137+
command=["sudo", "id"],
139138
)
140139
assert "uid=0(root)" in logs
141140

@@ -147,7 +146,7 @@ def test_sudo_path(container: TrackedContainer) -> None:
147146
tty=True,
148147
user="root",
149148
environment=["GRANT_SUDO=yes"],
150-
command=["start.sh", "sudo", "which", "jupyter"],
149+
command=["sudo", "which", "jupyter"],
151150
)
152151
assert logs.rstrip().endswith("/opt/conda/bin/jupyter")
153152

@@ -158,7 +157,7 @@ def test_sudo_path_without_grant(container: TrackedContainer) -> None:
158157
timeout=10,
159158
tty=True,
160159
user="root",
161-
command=["start.sh", "which", "jupyter"],
160+
command=["which", "jupyter"],
162161
)
163162
assert logs.rstrip().endswith("/opt/conda/bin/jupyter")
164163

@@ -173,7 +172,7 @@ def test_group_add(container: TrackedContainer) -> None:
173172
no_warnings=False,
174173
user="1010:1010",
175174
group_add=["users"], # Ensures write access to /home/jovyan
176-
command=["start.sh", "id"],
175+
command=["id"],
177176
)
178177
warnings = TrackedContainer.get_warnings(logs)
179178
assert len(warnings) == 1
@@ -191,7 +190,7 @@ def test_set_uid(container: TrackedContainer) -> None:
191190
timeout=5,
192191
no_warnings=False,
193192
user="1010",
194-
command=["start.sh", "id"],
193+
command=["id"],
195194
)
196195
assert "uid=1010(jovyan) gid=0(root)" in logs
197196
warnings = TrackedContainer.get_warnings(logs)
@@ -207,7 +206,7 @@ def test_set_uid_and_nb_user(container: TrackedContainer) -> None:
207206
user="1010",
208207
environment=["NB_USER=kitten"],
209208
group_add=["users"], # Ensures write access to /home/jovyan
210-
command=["start.sh", "id"],
209+
command=["id"],
211210
)
212211
assert "uid=1010(kitten) gid=0(root)" in logs
213212
warnings = TrackedContainer.get_warnings(logs)
@@ -236,7 +235,7 @@ def test_container_not_delete_bind_mount(
236235
"CHOWN_HOME=yes",
237236
],
238237
volumes={d: {"bind": "/home/jovyan/data", "mode": "rw"}},
239-
command=["start.sh", "ls"],
238+
command=["ls"],
240239
)
241240
assert p.read_text() == "some-content"
242241
assert len(list(tmp_path.iterdir())) == 1
@@ -259,7 +258,6 @@ def test_jupyter_env_vars_to_unset(
259258
"SECRET_FRUIT=mango",
260259
],
261260
command=[
262-
"start.sh",
263261
"bash",
264262
"-c",
265263
"echo I like ${FRUIT} and ${SECRET_FRUIT:-stuff}, and love ${SECRET_ANIMAL:-to keep secrets}!",
@@ -284,7 +282,26 @@ def test_secure_path(container: TrackedContainer, tmp_path: pathlib.Path) -> Non
284282
tty=True,
285283
user="root",
286284
volumes={p: {"bind": "/usr/bin/python", "mode": "ro"}},
287-
command=["start.sh", "python", "--version"],
285+
command=["python", "--version"],
288286
)
289287
assert "Wrong python" not in logs
290288
assert "Python" in logs
289+
290+
291+
def test_startsh_multiple_exec(container: TrackedContainer) -> None:
292+
"""If start.sh is executed multiple times check that configuration only occurs once."""
293+
logs = container.run_and_wait(
294+
timeout=10,
295+
no_warnings=False,
296+
tty=True,
297+
user="root",
298+
environment=["GRANT_SUDO=yes"],
299+
command=["start.sh", "sudo", "id"],
300+
)
301+
assert "uid=0(root)" in logs
302+
warnings = TrackedContainer.get_warnings(logs)
303+
assert len(warnings) == 1
304+
assert (
305+
"WARNING: start.sh is the default ENTRYPOINT, do not include it in CMD"
306+
in warnings[0]
307+
)

tests/minimal-notebook/test_nbconvert.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def test_nbconvert(
2828
timeout=30,
2929
volumes={str(host_data_dir): {"bind": cont_data_dir, "mode": "ro"}},
3030
tty=True,
31-
command=["start.sh", "bash", "-c", command],
31+
command=["bash", "-c", command],
3232
)
3333
expected_file = f"{output_dir}/{test_file}.{output_format}"
3434
assert expected_file in logs, f"Expected file {expected_file} not generated"

tests/package_helper.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def start_container(container: TrackedContainer) -> Container:
5555
LOGGER.info(f"Starting container {container.image_name} ...")
5656
return container.run_detached(
5757
tty=True,
58-
command=["start.sh", "bash", "-c", "sleep infinity"],
58+
command=["bash", "-c", "sleep infinity"],
5959
)
6060

6161
@staticmethod

tests/run_command.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,5 @@ def run_command(
1818
return container.run_and_wait(
1919
timeout=timeout,
2020
tty=True,
21-
command=["start.sh", "bash", "-c", command],
21+
command=["bash", "-c", command],
2222
)

tests/scipy-notebook/test_cython.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ def test_cython(container: TrackedContainer) -> None:
1616
volumes={str(host_data_dir): {"bind": cont_data_dir, "mode": "ro"}},
1717
tty=True,
1818
command=[
19-
"start.sh",
2019
"bash",
2120
"-c",
2221
# We copy our data to a temporary folder to be able to modify the directory

tests/scipy-notebook/test_extensions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,5 @@ def test_check_extension(container: TrackedContainer, extension: str) -> None:
3030
container.run_and_wait(
3131
timeout=10,
3232
tty=True,
33-
command=["start.sh", "jupyter", "labextension", "check", extension],
33+
command=["jupyter", "labextension", "check", extension],
3434
)

0 commit comments

Comments
 (0)