main
sh 266 lines 12 KB
Raw
1 #!/bin/sh
2 # Shell-level unit test for vllm-nonroot-entrypoint.sh.
3 #
4 # Runs on the host (no Docker, no GPU) by stubbing `vllm` with a shim that
5 # dumps its env + argv instead of actually serving. Exercises the wrapper's
6 # HOME/USER fallback behavior that can't be easily tested from buildkite
7 # (which would need a GPU to run `vllm serve --help`).
8 #
9 # Usage:
10 # bash docker/entrypoints/test_vllm_nonroot_entrypoint.sh
11 # Exits non-zero on the first failed assertion.
12
13 set -eu
14
15 SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
16 WRAPPER="${SCRIPT_DIR}/vllm-nonroot-entrypoint.sh"
17
18 if [ ! -x "$WRAPPER" ]; then
19 echo "FAIL: wrapper not found or not executable: $WRAPPER" >&2
20 exit 1
21 fi
22
23 WORKDIR="$(mktemp -d)"
24 trap 'rm -rf "$WORKDIR"' EXIT
25
26 # Stub `vllm` on PATH. It dumps env + argv + cwd to stdout so we can assert.
27 mkdir -p "$WORKDIR/bin"
28 cat > "$WORKDIR/bin/vllm" <<'EOF'
29 #!/bin/sh
30 echo "ARGV=$*"
31 echo "HOME=${HOME-__unset__}"
32 echo "USER=${USER-__unset__}"
33 echo "LOGNAME=${LOGNAME-__unset__}"
34 echo "PWD=$(pwd)"
35 EOF
36 chmod +x "$WORKDIR/bin/vllm"
37
38 run_wrapper() {
39 # Usage: run_wrapper <output_file> <env_kv>... -- <wrapper_arg>...
40 _out="$1"; shift
41 _env=""
42 while [ "${1:-}" != "--" ]; do
43 _env="$_env $1"; shift
44 done
45 shift
46 # shellcheck disable=SC2086 # word splitting is intentional: $_env holds multiple NAME=value assignments
47 env -i PATH="$WORKDIR/bin:/usr/bin:/bin" $_env "$WRAPPER" "$@" > "$_out"
48 }
49
50 fail() { echo "FAIL: $*" >&2; echo "--- stdout ---" >&2; cat "$1" >&2; exit 1; }
51
52 expect_default_home() {
53 _out="$1"
54 _case="$2"
55 if [ -w /home/vllm ]; then
56 expected_home="/home/vllm"
57 grep -q "^HOME=$expected_home\$" "$_out" \
58 || fail "$_out" "$_case: HOME not set to $expected_home"
59 else
60 expected_home="/tmp/vllm-home.XXXXXX"
61 grep -Eq '^HOME=/tmp/vllm-home\.[^/]+$' "$_out" \
62 || fail "$_out" "$_case: HOME not set to $expected_home"
63 fi
64 }
65
66 # -----------------------------------------------------------------------------
67 # Case 1: writable HOME and USER both set -> wrapper must leave them alone.
68 # -----------------------------------------------------------------------------
69 case1_home="$WORKDIR/case1-home"
70 mkdir -p "$case1_home"
71 out="$WORKDIR/case1.out"
72 run_wrapper "$out" "HOME=$case1_home" "USER=alice" "LOGNAME=alice" -- --model foo
73 grep -q "^HOME=$case1_home\$" "$out" || fail "$out" "case1: HOME not preserved"
74 grep -q "^USER=alice\$" "$out" || fail "$out" "case1: USER not preserved"
75 grep -q "^LOGNAME=alice\$" "$out" || fail "$out" "case1: LOGNAME not preserved"
76 grep -q "^ARGV=serve --model foo\$" "$out" || fail "$out" "case1: ARGV wrong"
77 echo "PASS: case1 (writable HOME + USER preserved)"
78
79 # -----------------------------------------------------------------------------
80 # Case 2: HOME unset -> falls back to /home/vllm if writable, else
81 # /tmp/vllm-home.XXXXXX.
82 # -----------------------------------------------------------------------------
83 # The wrapper checks whether the real /home/vllm exists and is writable. On
84 # dev machines /home/vllm typically does NOT exist, so the
85 # wrapper should fall to /tmp/vllm-home.XXXXXX.
86 out="$WORKDIR/case2.out"
87 run_wrapper "$out" -- --model bar
88 expect_default_home "$out" "case2"
89 grep -q "^USER=vllm\$" "$out" || fail "$out" "case2: USER not defaulted to vllm"
90 grep -q "^LOGNAME=vllm\$" "$out" || fail "$out" "case2: LOGNAME not defaulted to vllm"
91 grep -q "^ARGV=serve --model bar\$" "$out" || fail "$out" "case2: ARGV wrong"
92 echo "PASS: case2 (unset HOME falls back to $expected_home, USER defaulted)"
93
94 # -----------------------------------------------------------------------------
95 # Case 3: HOME set but unwritable -> must also fall back.
96 # -----------------------------------------------------------------------------
97 ro_home="$WORKDIR/ro-home"
98 mkdir -p "$ro_home"
99 chmod 0500 "$ro_home"
100 out="$WORKDIR/case3.out"
101 run_wrapper "$out" "HOME=$ro_home" -- --model baz
102 expect_default_home "$out" "case3"
103 grep -q "^USER=vllm\$" "$out" || fail "$out" "case3: USER not defaulted"
104 chmod 0700 "$ro_home"
105 echo "PASS: case3 (unwritable HOME overridden)"
106
107 # -----------------------------------------------------------------------------
108 # Case 4: USER set but LOGNAME unset -> LOGNAME mirrors USER.
109 # -----------------------------------------------------------------------------
110 case4_home="$WORKDIR/case4-home"
111 mkdir -p "$case4_home"
112 out="$WORKDIR/case4.out"
113 run_wrapper "$out" "HOME=$case4_home" "USER=carol" -- --model qux
114 grep -q "^USER=carol\$" "$out" || fail "$out" "case4: USER not preserved"
115 grep -q "^LOGNAME=carol\$" "$out" || fail "$out" "case4: LOGNAME not mirrored from USER"
116 echo "PASS: case4 (LOGNAME mirrors USER when unset)"
117
118 # -----------------------------------------------------------------------------
119 # Case 5: /etc/passwd is writable AND the current UID is not in it -> wrapper
120 # appends a synthetic entry. Uses the VLLM_PASSWD_FILE test hook so we don't
121 # touch the real /etc/passwd.
122 # -----------------------------------------------------------------------------
123 fake_passwd="$WORKDIR/fake-passwd"
124 : > "$fake_passwd" # empty file, current UID definitely not present
125 case5_home="$WORKDIR/case5-home"
126 mkdir -p "$case5_home"
127 out="$WORKDIR/case5.out"
128 run_wrapper "$out" "HOME=$case5_home" "VLLM_PASSWD_FILE=$fake_passwd" -- --model foo
129 current_uid="$(id -u)"
130 current_gid="$(id -g)"
131 expected_line="vllm:x:${current_uid}:${current_gid}:vllm:${case5_home}:/bin/bash"
132 grep -Fx "$expected_line" "$fake_passwd" > /dev/null \
133 || { echo "FAIL: case5: expected line not found in fake passwd:"; echo " expected: $expected_line"; echo " file contents:"; cat "$fake_passwd"; exit 1; }
134 echo "PASS: case5 (passwd entry appended for arbitrary UID)"
135
136 # -----------------------------------------------------------------------------
137 # Case 6: /etc/passwd is writable but current UID already has an entry ->
138 # wrapper must NOT duplicate the entry.
139 # -----------------------------------------------------------------------------
140 fake_passwd="$WORKDIR/fake-passwd-prepopulated"
141 printf 'vllm:x:%s:%s:vllm:/home/vllm:/bin/bash\n' "$current_uid" "$current_gid" > "$fake_passwd"
142 out="$WORKDIR/case6.out"
143 run_wrapper "$out" "HOME=$case5_home" "VLLM_PASSWD_FILE=$fake_passwd" -- --model foo
144 # wc may count 0 or 1 depending on trailing newline, so count lines
145 # matching our UID instead for a robust check.
146 uid_lines="$(grep -c ":${current_uid}:" "$fake_passwd" || true)"
147 [ "$uid_lines" = "1" ] \
148 || { echo "FAIL: case6: expected exactly one entry for UID $current_uid, got $uid_lines"; cat "$fake_passwd"; exit 1; }
149 echo "PASS: case6 (existing passwd entry not duplicated)"
150
151 # -----------------------------------------------------------------------------
152 # Case 7: /etc/passwd is NOT writable -> wrapper must NOT crash, just skip.
153 # Skipped when running as root, because root's DAC override means [ -w ... ]
154 # is always true regardless of mode bits -- the case can't be simulated.
155 # In the real deployment (non-root UID inside the container) this IS the
156 # relevant behavior and is what `_passwd_file is not writable` encodes.
157 # -----------------------------------------------------------------------------
158 if [ "$(id -u)" = "0" ]; then
159 echo "SKIP: case7 (running as root; DAC override makes unwritable check meaningless)"
160 else
161 fake_passwd="$WORKDIR/ro-passwd"
162 : > "$fake_passwd"
163 chmod 0444 "$fake_passwd"
164 out="$WORKDIR/case7.out"
165 run_wrapper "$out" "HOME=$case5_home" "VLLM_PASSWD_FILE=$fake_passwd" -- --model foo
166 # File must remain empty (no write happened) and the wrapper exec'd
167 # `vllm serve` successfully (stdout contains ARGV line).
168 [ ! -s "$fake_passwd" ] \
169 || { echo "FAIL: case7: RO passwd file was modified"; cat "$fake_passwd"; exit 1; }
170 grep -q "^ARGV=serve --model foo\$" "$out" || fail "$out" "case7: wrapper didn't exec vllm"
171 chmod 0600 "$fake_passwd"
172 echo "PASS: case7 (unwritable passwd file tolerated)"
173 fi
174
175 # -----------------------------------------------------------------------------
176 # Case 8: caller's writable CWD is preserved — wrapper must NOT chdir to HOME
177 # when cwd is usable. Protects relative-path workflows like
178 # `docker run -w /models ... --model ./llama.gguf`.
179 # -----------------------------------------------------------------------------
180 case8_home="$WORKDIR/case8-home"
181 mkdir -p "$case8_home"
182 case8_cwd="$WORKDIR/case8-cwd"
183 mkdir -p "$case8_cwd"
184 out="$WORKDIR/case8.out"
185 (cd "$case8_cwd" && run_wrapper "$out" "HOME=$case8_home" "USER=alice" "LOGNAME=alice" -- --model ./relpath)
186 grep -q "^PWD=$case8_cwd\$" "$out" \
187 || fail "$out" "case8: writable cwd not preserved (got $(grep '^PWD=' "$out"))"
188 grep -q "^ARGV=serve --model \\./relpath\$" "$out" \
189 || fail "$out" "case8: relative argv not preserved"
190 echo "PASS: case8 (writable cwd preserved; relative argv still resolves from caller's cwd)"
191
192 # -----------------------------------------------------------------------------
193 # Case 9: read-only cwd is ALSO preserved. A caller who mounts a read-only
194 # model directory at the container's cwd (e.g. `docker run -w /models` with
195 # /models bind-mounted ro) expects relative argv like `--model ./foo.gguf`
196 # to resolve against /models. An earlier version of this wrapper rewrote
197 # read-only cwd to $HOME and broke that workflow; this case guards against
198 # the regression returning.
199 # -----------------------------------------------------------------------------
200 case9_home="$WORKDIR/case9-home"
201 mkdir -p "$case9_home"
202 case9_ro="$WORKDIR/case9-ro"
203 mkdir -p "$case9_ro"
204 chmod 0555 "$case9_ro"
205 out="$WORKDIR/case9.out"
206 (cd "$case9_ro" && run_wrapper "$out" "HOME=$case9_home" "USER=alice" "LOGNAME=alice" -- --model ./foo)
207 grep -q "^PWD=$case9_ro\$" "$out" \
208 || fail "$out" "case9: read-only cwd was rewritten (got $(grep '^PWD=' "$out"))"
209 grep -q "^ARGV=serve --model \\./foo\$" "$out" \
210 || fail "$out" "case9: relative argv not preserved"
211 chmod 0700 "$case9_ro"
212 echo "PASS: case9 (read-only cwd preserved; relative argv still resolves from caller's cwd)"
213
214 # -----------------------------------------------------------------------------
215 # Case 10: truly inaccessible cwd (no search bit) DOES fall back to $HOME.
216 # Skipped as root because DAC override lets root cd into 0000 directories.
217 # -----------------------------------------------------------------------------
218 if [ "$(id -u)" = "0" ]; then
219 echo "SKIP: case10 (running as root; DAC override makes inaccessible cwd untestable)"
220 else
221 case10_home="$WORKDIR/case10-home"
222 mkdir -p "$case10_home"
223 case10_cwd="$WORKDIR/case10-cwd"
224 mkdir -p "$case10_cwd"
225 out="$WORKDIR/case10.out"
226 # Make cwd genuinely inaccessible (mode 0000 = no search bit -> cd .
227 # fails with EACCES). Use absolute paths for chmod so our own test
228 # cleanup still works without needing search perm on the dir.
229 (
230 cd "$case10_cwd"
231 chmod 0000 "$case10_cwd"
232 run_wrapper "$out" "HOME=$case10_home" "USER=alice" "LOGNAME=alice" -- --model foo
233 )
234 chmod 0700 "$case10_cwd"
235 grep -q "^PWD=$case10_home\$" "$out" \
236 || fail "$out" "case10: inaccessible cwd not overridden to HOME (got $(grep '^PWD=' "$out"))"
237 echo "PASS: case10 (inaccessible cwd falls back to \$HOME)"
238 fi
239
240 # -----------------------------------------------------------------------------
241 # Case 11: if /tmp cannot create a private fallback dir, wrapper uses /tmp as
242 # the last-resort HOME instead of leaving HOME empty under set -eu.
243 # -----------------------------------------------------------------------------
244 if [ -w /home/vllm ]; then
245 echo "SKIP: case11 (/home/vllm is writable; mktemp fallback path is not used)"
246 else
247 cat > "$WORKDIR/bin/mktemp" <<'EOF'
248 #!/bin/sh
249 exit 1
250 EOF
251 chmod +x "$WORKDIR/bin/mktemp"
252
253 out="$WORKDIR/case11.out"
254 run_wrapper "$out" -- --model no-mktemp
255 rm -f "$WORKDIR/bin/mktemp"
256
257 grep -q "^HOME=/tmp\$" "$out" \
258 || fail "$out" "case11: mktemp failure did not fall back to /tmp"
259 grep -q "^USER=vllm\$" "$out" || fail "$out" "case11: USER not defaulted"
260 grep -q "^LOGNAME=vllm\$" "$out" || fail "$out" "case11: LOGNAME not defaulted"
261 grep -q "^ARGV=serve --model no-mktemp\$" "$out" || fail "$out" "case11: ARGV wrong"
262 echo "PASS: case11 (mktemp failure falls back to /tmp)"
263 fi
264
265 echo ""
266 echo "ALL CASES PASSED."