Skip to content

Commit 985bd82

Browse files
authored
Merge pull request #1546 from maresb/fix-spurious-warning
Fix spurious warnings when using docker run --user=X
2 parents b4aab84 + edd0bf7 commit 985bd82

File tree

6 files changed

+133
-21
lines changed

6 files changed

+133
-21
lines changed

base-notebook/start.sh

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -172,49 +172,59 @@ if [ "$(id -u)" == 0 ] ; then
172172
# The container didn't start as the root user, so we will have to act as the
173173
# user we started as.
174174
else
175-
# Warn about misconfiguration of: desired username, user id, or group id
176-
if [[ -n "${NB_USER}" && "${NB_USER}" != "$(id -un)" ]]; then
177-
_log "WARNING: container must be started as root to change the desired user's name with NB_USER!"
178-
fi
179-
if [[ -n "${NB_UID}" && "${NB_UID}" != "$(id -u)" ]]; then
180-
_log "WARNING: container must be started as root to change the desired user's id with NB_UID!"
181-
fi
182-
if [[ -n "${NB_GID}" && "${NB_GID}" != "$(id -g)" ]]; then
183-
_log "WARNING: container must be started as root to change the desired user's group id with NB_GID!"
184-
fi
185-
186175
# Warn about misconfiguration of: granting sudo rights
187176
if [[ "${GRANT_SUDO}" == "1" || "${GRANT_SUDO}" == "yes" ]]; then
188177
_log "WARNING: container must be started as root to grant sudo permissions!"
189178
fi
190179

180+
JOVYAN_UID="$(id -u jovyan 2>/dev/null)" # The default UID for the jovyan user
181+
JOVYAN_GID="$(id -g jovyan 2>/dev/null)" # The default GID for the jovyan user
182+
191183
# Attempt to ensure the user uid we currently run as has a named entry in
192184
# the /etc/passwd file, as it avoids software crashing on hard assumptions
193185
# on such entry. Writing to the /etc/passwd was allowed for the root group
194186
# from the Dockerfile during build.
195187
#
196188
# ref: https://github.com/jupyter/docker-stacks/issues/552
197189
if ! whoami &> /dev/null; then
198-
_log "There is no entry in /etc/passwd for our UID. Attempting to fix..."
190+
_log "There is no entry in /etc/passwd for our UID=$(id -u). Attempting to fix..."
199191
if [[ -w /etc/passwd ]]; then
200192
_log "Renaming old jovyan user to nayvoj ($(id -u jovyan):$(id -g jovyan))"
201193

202194
# We cannot use "sed --in-place" since sed tries to create a temp file in
203195
# /etc/ and we may not have write access. Apply sed on our own temp file:
204196
sed --expression="s/^jovyan:/nayvoj:/" /etc/passwd > /tmp/passwd
205-
echo "jovyan:x:$(id -u):$(id -g):,,,:/home/jovyan:/bin/bash" >> /tmp/passwd
197+
echo "${NB_USER}:x:$(id -u):$(id -g):,,,:/home/jovyan:/bin/bash" >> /tmp/passwd
206198
cat /tmp/passwd > /etc/passwd
207199
rm /tmp/passwd
208200

209-
_log "Added new jovyan user ($(id -u):$(id -g)). Fixed UID!"
201+
_log "Added new ${NB_USER} user ($(id -u):$(id -g)). Fixed UID!"
202+
203+
if [[ "${NB_USER}" != "jovyan" ]]; then
204+
_log "WARNING: user is ${NB_USER} but home is /home/jovyan. You must run as root to rename the home directory!"
205+
fi
210206
else
211-
_log "WARNING: unable to fix missing /etc/passwd entry because we don't have write permission."
207+
_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\"."
212208
fi
213209
fi
214210

211+
# Warn about misconfiguration of: desired username, user id, or group id.
212+
# A misconfiguration occurs when the user modifies the default values of
213+
# NB_USER, NB_UID, or NB_GID, but we cannot update those values because we
214+
# are not root.
215+
if [[ "${NB_USER}" != "jovyan" && "${NB_USER}" != "$(id -un)" ]]; then
216+
_log "WARNING: container must be started as root to change the desired user's name with NB_USER=\"${NB_USER}\"!"
217+
fi
218+
if [[ "${NB_UID}" != "${JOVYAN_UID}" && "${NB_UID}" != "$(id -u)" ]]; then
219+
_log "WARNING: container must be started as root to change the desired user's id with NB_UID=\"${NB_UID}\"!"
220+
fi
221+
if [[ "${NB_GID}" != "${JOVYAN_GID}" && "${NB_GID}" != "$(id -g)" ]]; then
222+
_log "WARNING: container must be started as root to change the desired user's group id with NB_GID=\"${NB_GID}\"!"
223+
fi
224+
215225
# Warn if the user isn't able to write files to ${HOME}
216226
if [[ ! -w /home/jovyan ]]; then
217-
_log "WARNING: no write access to /home/jovyan. Try starting the container with group 'users' (100)."
227+
_log "WARNING: no write access to /home/jovyan. Try starting the container with group 'users' (100), e.g. using \"--group-add=users\"."
218228
fi
219229

220230
# NOTE: This hook is run as the user we started the container as!

base-notebook/test/test_container_options.py

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ def test_cli_args(container, http_client):
1616
resp.raise_for_status()
1717
logs = c.logs(stdout=True).decode("utf-8")
1818
LOGGER.debug(logs)
19+
assert "ERROR" not in logs
20+
warnings = [
21+
warning for warning in logs.split("\n") if warning.startswith("WARNING")
22+
]
23+
assert len(warnings) == 1
24+
assert warnings[0].startswith("WARNING: Jupyter Notebook deprecation notice")
1925
assert "login_submit" not in resp.text
2026

2127

@@ -24,7 +30,7 @@ def test_unsigned_ssl(container, http_client):
2430
"""Container should generate a self-signed SSL certificate
2531
and notebook server should use it to enable HTTPS.
2632
"""
27-
container.run(environment=["GEN_CERT=yes"])
33+
c = container.run(environment=["GEN_CERT=yes"])
2834
# NOTE: The requests.Session backing the http_client fixture does not retry
2935
# properly while the server is booting up. An SSL handshake error seems to
3036
# abort the retry logic. Forcing a long sleep for the moment until I have
@@ -33,6 +39,13 @@ def test_unsigned_ssl(container, http_client):
3339
resp = http_client.get("https://localhost:8888", verify=False)
3440
resp.raise_for_status()
3541
assert "login_submit" in resp.text
42+
logs = c.logs(stdout=True).decode("utf-8")
43+
assert "ERROR" not in logs
44+
warnings = [
45+
warning for warning in logs.split("\n") if warning.startswith("WARNING")
46+
]
47+
assert len(warnings) == 1
48+
assert warnings[0].startswith("WARNING: Jupyter Notebook deprecation notice")
3649

3750

3851
def test_uid_change(container):
@@ -45,6 +58,9 @@ def test_uid_change(container):
4558
)
4659
# usermod is slow so give it some time
4760
rv = c.wait(timeout=120)
61+
logs = c.logs(stdout=True).decode("utf-8")
62+
assert "ERROR" not in logs
63+
assert "WARNING" not in logs
4864
assert rv == 0 or rv["StatusCode"] == 0
4965
assert "uid=1010(jovyan)" in c.logs(stdout=True).decode("utf-8")
5066

@@ -60,6 +76,8 @@ def test_gid_change(container):
6076
rv = c.wait(timeout=10)
6177
assert rv == 0 or rv["StatusCode"] == 0
6278
logs = c.logs(stdout=True).decode("utf-8")
79+
assert "ERROR" not in logs
80+
assert "WARNING" not in logs
6381
assert "gid=110(jovyan)" in logs
6482
assert "groups=110(jovyan),100(users)" in logs
6583

@@ -79,6 +97,8 @@ def test_nb_user_change(container):
7997
time.sleep(10)
8098
LOGGER.info(f"Checking if the user is changed to {nb_user} by the start script ...")
8199
output = running_container.logs(stdout=True).decode("utf-8")
100+
assert "ERROR" not in output
101+
assert "WARNING" not in output
82102
assert (
83103
f"username: jovyan -> {nb_user}" in output
84104
), f"User is not changed to {nb_user}"
@@ -134,6 +154,8 @@ def test_chown_extra(container):
134154
rv = c.wait(timeout=120)
135155
assert rv == 0 or rv["StatusCode"] == 0
136156
logs = c.logs(stdout=True).decode("utf-8")
157+
assert "ERROR" not in logs
158+
assert "WARNING" not in logs
137159
assert "/home/jovyan/.bashrc:1010:101" in logs
138160
assert "/opt/conda/bin/jupyter:1010:101" in logs
139161

@@ -156,6 +178,8 @@ def test_chown_home(container):
156178
rv = c.wait(timeout=120)
157179
assert rv == 0 or rv["StatusCode"] == 0
158180
logs = c.logs(stdout=True).decode("utf-8")
181+
assert "ERROR" not in logs
182+
assert "WARNING" not in logs
159183
assert "/home/kitten/.bashrc:1010:101" in logs
160184

161185

@@ -169,7 +193,10 @@ def test_sudo(container):
169193
)
170194
rv = c.wait(timeout=10)
171195
assert rv == 0 or rv["StatusCode"] == 0
172-
assert "uid=0(root)" in c.logs(stdout=True).decode("utf-8")
196+
logs = c.logs(stdout=True).decode("utf-8")
197+
assert "ERROR" not in logs
198+
assert "WARNING" not in logs
199+
assert "uid=0(root)" in logs
173200

174201

175202
def test_sudo_path(container):
@@ -183,6 +210,8 @@ def test_sudo_path(container):
183210
rv = c.wait(timeout=10)
184211
assert rv == 0 or rv["StatusCode"] == 0
185212
logs = c.logs(stdout=True).decode("utf-8")
213+
assert "ERROR" not in logs
214+
assert "WARNING" not in logs
186215
assert logs.rstrip().endswith("/opt/conda/bin/jupyter")
187216

188217

@@ -196,24 +225,75 @@ def test_sudo_path_without_grant(container):
196225
rv = c.wait(timeout=10)
197226
assert rv == 0 or rv["StatusCode"] == 0
198227
logs = c.logs(stdout=True).decode("utf-8")
228+
assert "ERROR" not in logs
229+
assert "WARNING" not in logs
199230
assert logs.rstrip().endswith("/opt/conda/bin/jupyter")
200231

201232

202-
def test_group_add(container, tmpdir):
233+
def test_group_add(container):
203234
"""Container should run with the specified uid, gid, and secondary
204-
group.
235+
group. It won't be possible to modify /etc/passwd since gid is nonzero, so
236+
additionally verify that setting gid=0 is suggested in a warning.
205237
"""
206238
c = container.run(
207239
user="1010:1010",
208-
group_add=["users"],
240+
group_add=["users"], # Ensures write access to /home/jovyan
209241
command=["start.sh", "id"],
210242
)
211243
rv = c.wait(timeout=5)
212244
assert rv == 0 or rv["StatusCode"] == 0
213245
logs = c.logs(stdout=True).decode("utf-8")
246+
assert "ERROR" not in logs
247+
warnings = [
248+
warning for warning in logs.split("\n") if warning.startswith("WARNING")
249+
]
250+
assert len(warnings) == 1
251+
assert "Try setting gid=0" in warnings[0]
214252
assert "uid=1010 gid=1010 groups=1010,100(users)" in logs
215253

216254

255+
def test_set_uid(container):
256+
"""Container should run with the specified uid and NB_USER.
257+
The /home/jovyan directory will not be writable since it's owned by 1000:users.
258+
Additionally verify that "--group-add=users" is suggested in a warning to restore
259+
write access.
260+
"""
261+
c = container.run(
262+
user="1010",
263+
command=["start.sh", "id"],
264+
)
265+
rv = c.wait(timeout=5)
266+
assert rv == 0 or rv["StatusCode"] == 0
267+
logs = c.logs(stdout=True).decode("utf-8")
268+
assert "ERROR" not in logs
269+
assert "uid=1010(jovyan) gid=0(root)" in logs
270+
warnings = [
271+
warning for warning in logs.split("\n") if warning.startswith("WARNING")
272+
]
273+
assert len(warnings) == 1
274+
assert "--group-add=users" in warnings[0]
275+
276+
277+
def test_set_uid_and_nb_user(container):
278+
"""Container should run with the specified uid and NB_USER."""
279+
c = container.run(
280+
user="1010",
281+
environment=["NB_USER=kitten"],
282+
group_add=["users"], # Ensures write access to /home/jovyan
283+
command=["start.sh", "id"],
284+
)
285+
rv = c.wait(timeout=5)
286+
assert rv == 0 or rv["StatusCode"] == 0
287+
logs = c.logs(stdout=True).decode("utf-8")
288+
assert "ERROR" not in logs
289+
assert "uid=1010(kitten) gid=0(root)" in logs
290+
warnings = [
291+
warning for warning in logs.split("\n") if warning.startswith("WARNING")
292+
]
293+
assert len(warnings) == 1
294+
assert "user is kitten but home is /home/jovyan" in warnings[0]
295+
296+
217297
def test_container_not_delete_bind_mount(container, tmp_path):
218298
"""Container should not delete host system files when using the (docker)
219299
-v bind mount flag and mapping to /home/jovyan.
@@ -235,6 +315,9 @@ def test_container_not_delete_bind_mount(container, tmp_path):
235315
command=["start.sh", "ls"],
236316
)
237317
rv = c.wait(timeout=5)
318+
logs = c.logs(stdout=True).decode("utf-8")
319+
assert "ERROR" not in logs
320+
assert "WARNING" not in logs
238321
assert rv == 0 or rv["StatusCode"] == 0
239322
assert p.read_text() == "some-content"
240323
assert len(list(tmp_path.iterdir())) == 1
@@ -264,4 +347,6 @@ def test_jupyter_env_vars_to_unset_as_root(container, enable_root):
264347
rv = c.wait(timeout=10)
265348
assert rv == 0 or rv["StatusCode"] == 0
266349
logs = c.logs(stdout=True).decode("utf-8")
350+
assert "ERROR" not in logs
351+
assert "WARNING" not in logs
267352
assert "I like bananas and stuff, and love to keep secrets!" in logs

base-notebook/test/test_package_managers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ def test_package_manager(container, package_manager, version_arg):
2929
rv = c.wait(timeout=5)
3030
logs = c.logs(stdout=True).decode("utf-8")
3131
LOGGER.debug(logs)
32+
assert "ERROR" not in logs
33+
assert "WARNING" not in logs
3234
assert (
3335
rv == 0 or rv["StatusCode"] == 0
3436
), f"Package manager {package_manager} not working"

base-notebook/test/test_pandoc.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,7 @@ def test_pandoc(container):
1414
)
1515
c.wait(timeout=10)
1616
logs = c.logs(stdout=True).decode("utf-8")
17+
assert "ERROR" not in logs
18+
assert "WARNING" not in logs
1719
LOGGER.debug(logs)
1820
assert "<p><strong>BOLD</strong></p>" in logs

base-notebook/test/test_python.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ def test_python_version(container, python_next_version="3.10"):
1616
)
1717
cmd = c.exec_run("python --version")
1818
output = cmd.output.decode("utf-8")
19+
assert "ERROR" not in output
20+
assert "WARNING" not in output
1921
actual_python_version = version.parse(output.split()[1])
2022
assert actual_python_version < version.parse(
2123
python_next_version

base-notebook/test/test_start_container.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ def test_start_notebook(container, http_client, env, expected_server):
2727
resp = http_client.get("http://localhost:8888")
2828
logs = c.logs(stdout=True).decode("utf-8")
2929
LOGGER.debug(logs)
30+
assert "ERROR" not in logs
31+
if expected_server != "notebook":
32+
assert "WARNING" not in logs
33+
else:
34+
warnings = [
35+
warning for warning in logs.split("\n") if warning.startswith("WARNING")
36+
]
37+
assert len(warnings) == 1
38+
assert warnings[0].startswith("WARNING: Jupyter Notebook deprecation notice")
3039
assert resp.status_code == 200, "Server is not listening"
3140
assert (
3241
f"Executing the command: jupyter {expected_server}" in logs
@@ -51,4 +60,6 @@ def test_tini_entrypoint(container, pid=1, command="tini"):
5160
# Select the PID 1 and get the corresponding command
5261
cmd = c.exec_run(f"ps -p {pid} -o comm=")
5362
output = cmd.output.decode("utf-8").strip("\n")
63+
assert "ERROR" not in output
64+
assert "WARNING" not in output
5465
assert output == command, f"{command} shall be launched as pid {pid}, got {output}"

0 commit comments

Comments
 (0)