Skip to content

Commit 044421d

Browse files
committed
Run ecflow_ui as the host user and make the launcher configurable
launch-ecflow_ui.sh now recreates the invoking user's account (ECF_RUN_USER/UID/GID) and drops privileges via setpriv before exec, so ecflow's get_login_name() matches the host. It also honours ECF_UI_NO_PROXYCHAINS to run directly, and forces IPv4 resolution of the proxy host to avoid proxychains "localnet address error". docker_ecflow_ui.sh drives the entrypoint in both proxychains modes and adds --run-as-root, --host-launcher FILE, and --proxy-host.
1 parent 2531077 commit 044421d

2 files changed

Lines changed: 148 additions & 10 deletions

File tree

releng/dockit/docker_ecflow_ui.sh

Lines changed: 107 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,30 @@ CFG_DIR="${HOME}/.ecflow_ui_v5"
1919
IMAGE="eccr.ecmwf.int/ecflow-dev-environments/ecflow-all-dev:latest"
2020
PLATFORM="linux/amd64"
2121
PROXYCHAINS="off"
22-
# An ECF_AUTHTOKENS already set in the environment this script runs in is equivalent to passing
22+
#
23+
# When ECF_AUTHTOKENS is set in the environment this script runs, it is equivalent to passing
2324
# --authtokens with that value; the flag, if also given, takes precedence over it.
25+
#
2426
AUTHTOKENS="${ECF_AUTHTOKENS:-}"
27+
#
28+
# Optional override for the host the container's entrypoint (launch-ecflow_ui.sh) uses as its SOCKS
29+
# proxy / X11 / localnet address. Empty means: let the entrypoint resolve host.docker.internal
30+
# itself (it forces IPv4). Set it (e.g. --proxy-host 192.168.65.254) to pin that address, for
31+
# instance against an older image whose entrypoint still resolves an IPv6 address.
32+
#
33+
PROXY_HOST="${PROXY_HOST:-}"
34+
#
35+
# By default the container mirrors the invoking host user (same name/uid/gid): the launcher
36+
# recreates that account inside the container and drops privileges to it, so ecflow reports the same
37+
# login user as on the host (its get_login_name() is getpwuid(getuid())). --run-as-root keeps root.
38+
#
39+
RUN_AS_ROOT="false"
40+
#
41+
# Optional path to a host launcher script to mount over the image's baked-in entrypoint
42+
# (launch-ecflow_ui.sh), so launcher fixes take effect without rebuilding the image.
43+
# Empty means: use the launcher baked into the container as-is.
44+
#
45+
HOST_LAUNCHER=""
2546

2647
usage() {
2748
cat <<'EOF'
@@ -42,19 +63,43 @@ Options:
4263
the calling environment is used as the default for this option, so
4364
having it exported is equivalent to passing --authtokens explicitly;
4465
an explicit --authtokens overrides it.
66+
--proxy-host HOST Override the address the container's entrypoint uses for its SOCKS proxy
67+
and X11/localnet exclusion, exposed as PROXY_HOST (default: unset, i.e.
68+
the entrypoint resolves host.docker.internal itself, forcing IPv4). A
69+
PROXY_HOST set in the calling environment is used as the default. Pin it
70+
(e.g. 192.168.65.254) against an older image whose entrypoint still
71+
resolves an IPv6 address and fails with "localnet address error".
72+
--run-as-root Run the container as root instead of mirroring the invoking host user.
73+
By default the host user's name/uid/gid are recreated inside the
74+
container and ecflow_ui runs as that user, so ecflow reports the same
75+
login user (getpwuid(getuid())) as on the host.
76+
--host-launcher FILE
77+
Mount FILE (a host launcher script) over the image's baked-in entrypoint
78+
launch-ecflow_ui.sh, so launcher fixes take effect without rebuilding the
79+
image. Default: unset, i.e. the launcher baked into the container is used.
4580
-h, --help Show this help and exit
4681
4782
Prerequisites (macOS):
4883
- An X server (e.g. XQuartz) running, with 'xhost +' applied (this script also applies it).
4984
- If --proxychains=on, an SSH SOCKS tunnel bound to more than loopback, left open in another
5085
terminal:
51-
ssh -g -D 9050 user@hpc-gateway
86+
ssh [-v] -g -C -N -D 9050 user@hpc-gateway
87+
-v for verbose output
88+
-g to allow remote hosts to connect (n.b. the container acts as a remote host)
89+
-C for compression
90+
-N to not run a command
91+
-D 9050 to open a SOCKS tunnel on port 9050 (the port baked into the image's entrypoint).
92+
93+
Notes:
94+
- With --host-launcher FILE, FILE is mounted over the image's entrypoint (both proxychains modes),
95+
allowing to customise the launcher without rebuilding the image.
5296
5397
Examples:
5498
./docker_ecflow_ui.sh
5599
./docker_ecflow_ui.sh --proxychains=on
56100
./docker_ecflow_ui.sh -c ~/cfg/ecflow_ui -i ecflow-all-dev:2026.1
57101
./docker_ecflow_ui.sh --authtokens ~/.ecflow_server.tokens
102+
./docker_ecflow_ui.sh --host-launcher ./ecflow-all/launch-ecflow_ui.sh
58103
ECF_AUTHTOKENS=~/.ecflow_server.tokens ./docker_ecflow_ui.sh
59104
curl -fsSL <url>/docker_ecflow_ui.sh | bash -s -- --proxychains=on
60105
EOF
@@ -98,6 +143,25 @@ while [ $# -gt 0 ]; do
98143
AUTHTOKENS="${1#*=}"
99144
shift
100145
;;
146+
--proxy-host)
147+
PROXY_HOST="$2"
148+
shift 2
149+
;;
150+
--proxy-host=*)
151+
PROXY_HOST="${1#*=}"
152+
shift
153+
;;
154+
--run-as-root)
155+
RUN_AS_ROOT="true"
156+
;;
157+
--host-launcher)
158+
HOST_LAUNCHER="$2"
159+
shift 2
160+
;;
161+
--host-launcher=*)
162+
HOST_LAUNCHER="${1#*=}"
163+
shift
164+
;;
101165
-h|--help)
102166
usage
103167
exit 0
@@ -132,11 +196,11 @@ else
132196
echo "docker_ecflow_ui.sh: warning: 'xhost' not found; skipping X11 access control setup" >&2
133197
fi
134198

135-
# proxychains=off bypasses the image's own ENTRYPOINT (launch-ecflow_ui.sh, which always sets up
136-
# proxychains4) and runs ecflow_ui directly instead.
137-
# Built as a single array, always with at least one element, rather than splicing in a separate
138-
# (possibly empty) array of --entrypoint args: bash 3.2 (macOS's default /bin/bash) raises
139-
# "unbound variable" under `set -u` when expanding "${arr[@]}" on a genuinely empty array.
199+
#
200+
# The image's ENTRYPOINT (launch-ecflow_ui.sh) is used in both modes. proxychains=off disables the
201+
# proxychains setup via the environment rather than replacing the entrypoint, so the launcher can
202+
# still recreate the run-as-user account and drop privileges.
203+
#
140204
DOCKER_ARGS=(
141205
--rm
142206
-P
@@ -145,7 +209,36 @@ DOCKER_ARGS=(
145209
-v "${CFG_DIR}:${CFG_DIR}"
146210
)
147211
if [ "${PROXYCHAINS}" = "off" ]; then
148-
DOCKER_ARGS+=(--entrypoint /usr/local/bin/ecflow_ui)
212+
DOCKER_ARGS+=(-e ECF_UI_NO_PROXYCHAINS=1)
213+
fi
214+
215+
#
216+
# Setup the host user inside the container (default), so that ecflow reports the same login user
217+
# as on the host. The launcher recreates this account and drops privileges to it.
218+
# --run-as-root skips this and leaves the container as root.
219+
#
220+
if [ "${RUN_AS_ROOT}" = "false" ]; then
221+
DOCKER_ARGS+=(
222+
-e "ECF_RUN_USER=$(id -un)"
223+
-e "ECF_RUN_UID=$(id -u)"
224+
-e "ECF_RUN_GID=$(id -g)"
225+
)
226+
fi
227+
228+
#
229+
# When --host-launcher FILE is given, override the image's baked-in entrypoint (launch-ecflow_ui.sh)
230+
# with FILE, so launcher fixes take effect without rebuilding the image. The launcher is the
231+
# entrypoint in both proxychains modes. When unset, the launcher baked into the container is used.
232+
#
233+
if [ -n "${HOST_LAUNCHER}" ]; then
234+
if [ ! -f "${HOST_LAUNCHER}" ]; then
235+
echo "docker_ecflow_ui.sh: --host-launcher file '${HOST_LAUNCHER}' does not exist" >&2
236+
exit 1
237+
fi
238+
HOST_LAUNCHER_DIR=$(cd "$(dirname "${HOST_LAUNCHER}")" && pwd)
239+
HOST_LAUNCHER="${HOST_LAUNCHER_DIR}/$(basename "${HOST_LAUNCHER}")"
240+
echo "docker_ecflow_ui.sh: using host launcher (overriding image entrypoint): ${HOST_LAUNCHER}" >&2
241+
DOCKER_ARGS+=(-v "${HOST_LAUNCHER}:/opt/local/bin/launch-ecflow_ui.sh:ro")
149242
fi
150243

151244
if [ -n "${AUTHTOKENS}" ]; then
@@ -160,6 +253,12 @@ if [ -n "${AUTHTOKENS}" ]; then
160253
DOCKER_ARGS+=(-v "${AUTHTOKENS}:${AUTHTOKENS}:ro" -e "ECF_AUTHTOKENS=${AUTHTOKENS}")
161254
fi
162255

256+
# Only passed when set, so the container's entrypoint keeps resolving host.docker.internal (IPv4)
257+
# by default; an explicit --proxy-host pins PROXY_HOST instead.
258+
if [ -n "${PROXY_HOST}" ]; then
259+
DOCKER_ARGS+=(-e "PROXY_HOST=${PROXY_HOST}")
260+
fi
261+
163262
# No -i/-t: ecflow_ui is a GUI app driven over X11, not an interactive TTY program, and dropping
164263
# them keeps this working when stdin is not a terminal - as when this script is itself piped into
165264
# bash via curl. docker run still streams stdout/stderr to this terminal and blocks until exit.

releng/dockit/ecflow-all/launch-ecflow_ui.sh

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,41 @@ set -e
44
set -u
55
set -o pipefail
66

7+
#
8+
# Allow to, by default, run ecflow_ui as a specific (host) user instead of root.
9+
# This can be opted out of by setting ECF_UI_NO_PROXYCHAINS, which will skip all proxychains setup and run ecflow_ui straight away, under the optional privilege drop.
10+
#
11+
# When ECF_RUN_UID is set, a matching group/passwd entry is created and privileges are dropped to
12+
# it (via setpriv) before ecflow_ui is exec'd, so that ecflow's get_login_name() -- which is just
13+
# getpwuid(getuid())->pw_name, ignoring $USER/$LOGNAME -- reports that user, mirroring how the
14+
# client identifies the user on the host. root is retained until the final exec because writing
15+
# /etc/proxychains4.conf below requires it. ECF_RUN_USER is required when ECF_RUN_UID is set;
16+
# ECF_RUN_GID defaults to ECF_RUN_UID.
17+
#
18+
run_as_user_prefix=()
19+
if [ -n "${ECF_RUN_UID:-}" ]; then
20+
run_as_user="${ECF_RUN_USER:?launch-ecflow_ui.sh: ECF_RUN_USER must be set when ECF_RUN_UID is set}"
21+
run_as_gid="${ECF_RUN_GID:-${ECF_RUN_UID}}"
22+
if ! getent group "${run_as_gid}" >/dev/null; then
23+
echo "${run_as_user}:x:${run_as_gid}:" >> /etc/group
24+
fi
25+
if ! getent passwd "${ECF_RUN_UID}" >/dev/null; then
26+
echo "${run_as_user}:x:${ECF_RUN_UID}:${run_as_gid}::/home/${run_as_user}:/bin/sh" >> /etc/passwd
27+
fi
28+
install -d -o "${ECF_RUN_UID}" -g "${run_as_gid}" "/home/${run_as_user}"
29+
# The ecflow_ui wrapper uses these for its temp/log paths; setpriv does not set them.
30+
export HOME="/home/${run_as_user}" USER="${run_as_user}" LOGNAME="${run_as_user}"
31+
run_as_user_prefix=(setpriv --reuid "${ECF_RUN_UID}" --regid "${run_as_gid}" --init-groups)
32+
fi
33+
34+
#
35+
# When proxychains is not wanted (direct connections, i.e. --proxychains=off), skip all proxychains
36+
# setup and run ecflow_ui straight away, under the optional privilege drop.
37+
#
38+
if [ -n "${ECF_UI_NO_PROXYCHAINS:-}" ]; then
39+
exec "${run_as_user_prefix[@]}" /usr/local/bin/ecflow_ui "$@"
40+
fi
41+
742
# Resolve the SOCKS5 proxy host (the SSH tunnel opened on the Docker host) to a literal IP address.
843
# proxychains-ng refuses a non-numeric address for the first proxy in a chain, since that hop is
944
# dialled directly, before any tunnel exists through which a hostname could be resolved remotely.
@@ -12,7 +47,11 @@ set -o pipefail
1247
PROXY_HOST=${PROXY_HOST:-host.docker.internal}
1348
PROXY_PORT=${PROXY_PORT:-9050}
1449

15-
PROXY_HOST_IP=$(getent hosts "${PROXY_HOST}" | awk '{ print $1; exit }')
50+
# Force IPv4 resolution: 'getent hosts' may return an IPv6 address first (recent Docker Desktop
51+
# hands back e.g. fdc4:...::254 for host.docker.internal), but the proxychains SOCKS proxy line and
52+
# the /24 'localnet' exclusion computed below both assume a dotted-quad IPv4 address - an IPv6
53+
# result yields a malformed 'localnet ...' line and proxychains aborts with "localnet address error".
54+
PROXY_HOST_IP=$(getent ahostsv4 "${PROXY_HOST}" | awk '{ print $1; exit }')
1655
if [ -z "${PROXY_HOST_IP}" ]; then
1756
echo "launch-ecflow_ui.sh: could not resolve ${PROXY_HOST} to an IP address" >&2
1857
exit 1
@@ -46,4 +85,4 @@ if [ -n "${DISPLAY:-}" ]; then
4685
export DISPLAY="${PROXY_HOST_IP}:${DISPLAY#*:}"
4786
fi
4887

49-
exec proxychains4 -f /etc/proxychains4.conf /usr/local/bin/ecflow_ui "$@"
88+
exec "${run_as_user_prefix[@]}" proxychains4 -f /etc/proxychains4.conf /usr/local/bin/ecflow_ui "$@"

0 commit comments

Comments
 (0)