Skip to content

Commit 6b96076

Browse files
committed
Convert run-hooks to python
- Primarily, implement the `source` method to emulate behavior of source command in bash - This won't actually be effective until start.sh is rewritten as well
1 parent d6519ac commit 6b96076

File tree

3 files changed

+114
-43
lines changed

3 files changed

+114
-43
lines changed

images/docker-stacks-foundation/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ ENTRYPOINT ["tini", "-g", "--"]
127127
CMD ["start.sh"]
128128

129129
# Copy local files as late as possible to avoid cache busting
130-
COPY run-hooks.sh start.sh /usr/local/bin/
130+
COPY run-hooks.py run-hooks.sh start.sh /usr/local/bin/
131131

132132
USER root
133133

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
#!/usr/bin/env python
2+
# Copyright (c) Jupyter Development Team.
3+
# Distributed under the terms of the Modified BSD License.
4+
5+
# The run-hooks.sh script looks for *.sh scripts to source
6+
# and executable files to run within a passed directory
7+
import os
8+
from textwrap import dedent
9+
import json
10+
import tempfile
11+
import sys
12+
import subprocess
13+
from pathlib import PosixPath
14+
15+
16+
def source(path: PosixPath):
17+
"""
18+
Emulate the bash `source` command accurately
19+
20+
When used in bash, `source` executes the passed file in the current 'context'
21+
of the script from where it is called. This primarily deals with how
22+
bash (and thus environment variables) are modified.
23+
24+
1. Any bash variables (particularly any set via `export`) are passed on to the
25+
sourced script as their values are at the point source is called
26+
2. The sourced script can itself use `export` to affect the bash variables of the
27+
parent script that called it.
28+
29+
(2) is the primary difference between `source` and just calling a shell script,
30+
and makes it possible for a set of scripts running in sequence to share data by
31+
passing bash variables across with `export`.
32+
33+
Given bash variables are environment variables, we will simply look for all modified
34+
environment variables in the script we have sourced, and update the calling python
35+
script's environment variables to match.
36+
37+
Args:
38+
path (PosixPath): Valid bash script to source
39+
"""
40+
# We start a bash process and have it `source` the script we are given. Then, we
41+
# use python (for convenience) to dump the environment variables from the bash process into
42+
# json (we could use `env` but then handling multiline variable values becomes a nightmare).
43+
# The json is written to a temporary file we create. We read this json, and update our python
44+
# process' environment variable with whatever we get back from bash.
45+
with tempfile.NamedTemporaryFile() as bash_file, tempfile.NamedTemporaryFile() as py_file, tempfile.NamedTemporaryFile() as env_vars_file:
46+
py_file.write(
47+
dedent(
48+
f"""
49+
import os
50+
import json
51+
with(open("{env_vars_file.name}", "w")) as f:
52+
json.dump(dict(os.environ), f)
53+
"""
54+
).encode()
55+
)
56+
py_file.flush()
57+
58+
bash_file.write(
59+
dedent(
60+
f"""
61+
#!/bin/bash
62+
source {path}
63+
python {py_file.name}
64+
"""
65+
).encode()
66+
)
67+
bash_file.flush()
68+
69+
run = subprocess.run(["/bin/bash", bash_file.name])
70+
71+
if run.returncode != 0:
72+
print(
73+
f"{path} has failed with return code {run.returncode}, continuing execution"
74+
)
75+
return
76+
77+
env_vars = json.load(env_vars_file)
78+
os.environ.update(env_vars)
79+
80+
81+
if len(sys.argv) != 2:
82+
print("Should pass exactly one directory")
83+
sys.exit(1)
84+
85+
hooks_directory = PosixPath(sys.argv[1])
86+
87+
if not hooks_directory.exists():
88+
print(f"Directory {hooks_directory} does not exist")
89+
90+
if not hooks_directory.is_dir():
91+
print(f"{hooks_directory} is not a directory")
92+
93+
print(f"Running hooks in: {hooks_directory} as {os.getuid()} gid: {os.getgid()}")
94+
95+
for f in hooks_directory.iterdir():
96+
if f.suffix == ".sh":
97+
print(f"Sourcing shell script: {f}")
98+
source(f)
99+
elif os.access(f, os.X_OK):
100+
print(f"Running executable: {f}")
101+
run = subprocess.run([str(f)])
102+
if run.returncode != 0:
103+
print(
104+
f"{f} has failed with return code {run.returncode}, continuing execution"
105+
)
106+
else:
107+
print(f"Ignoring non-executable file {f}")
108+
109+
110+
print(f"Done running hooks in: {hooks_directory}")
111+
112+
print(os.environ['HELLO'])

images/docker-stacks-foundation/run-hooks.sh

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,45 +2,4 @@
22
# Copyright (c) Jupyter Development Team.
33
# Distributed under the terms of the Modified BSD License.
44

5-
# The run-hooks.sh script looks for *.sh scripts to source
6-
# and executable files to run within a passed directory
7-
8-
if [ "$#" -ne 1 ]; then
9-
echo "Should pass exactly one directory"
10-
return 1
11-
fi
12-
13-
if [[ ! -d "${1}" ]] ; then
14-
echo "Directory ${1} doesn't exist or is not a directory"
15-
return 1
16-
fi
17-
18-
echo "Running hooks in: ${1} as uid: $(id -u) gid: $(id -g)"
19-
for f in "${1}/"*; do
20-
# Hadling a case when the directory is empty
21-
[ -e "${f}" ] || continue
22-
case "${f}" in
23-
*.sh)
24-
echo "Sourcing shell script: ${f}"
25-
# shellcheck disable=SC1090
26-
source "${f}"
27-
# shellcheck disable=SC2181
28-
if [ $? -ne 0 ] ; then
29-
echo "${f} has failed, continuing execution"
30-
fi
31-
;;
32-
*)
33-
if [ -x "${f}" ] ; then
34-
echo "Running executable: ${f}"
35-
"${f}"
36-
# shellcheck disable=SC2181
37-
if [ $? -ne 0 ] ; then
38-
echo "${f} has failed, continuing execution"
39-
fi
40-
else
41-
echo "Ignoring non-executable: ${f}"
42-
fi
43-
;;
44-
esac
45-
done
46-
echo "Done running hooks in: ${1}"
5+
exec /usr/local/bin/run-hooks.py "$@"

0 commit comments

Comments
 (0)