Files
windmill/docker/entrypoint-extra.sh
T
Ruben Fiszel d9d6ec82ab feat: register mounted CA certificates in windmill_extra at startup (#10545)
* feat: register mounted CA certificates in windmill_extra at startup

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: only claim a CA update when update-ca-certificates can read the mount

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: detect mounted CA certificates the way update-ca-certificates finds them

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 19:13:29 +00:00

180 lines
6.8 KiB
Bash

#!/bin/bash
set -e
# Windmill Extra Services Entrypoint
# Starts LSP, Multiplayer, and Debugger services based on environment variables
# Track PIDs for cleanup
PIDS=()
cleanup() {
echo "[entrypoint] Shutting down services..."
for pid in "${PIDS[@]}"; do
if kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
fi
done
wait
echo "[entrypoint] All services stopped"
exit 0
}
trap cleanup SIGTERM SIGINT
# An arbitrary non-root UID gets HOME=/ and cannot write the image's 0700 /root, so
# redirect $HOME before anything writes under it (netrc below, plus the bun/npm/go
# caches in the services). Keep the fallback UID-scoped: a leftover dir from a
# different UID on a shared /tmp is not writable. Root keeps HOME=/root.
HOME="${HOME:-/root}"
if [ ! -w "$HOME" ]; then
echo "[entrypoint] HOME=$HOME is not writable for UID $(id -u), using HOME=/tmp/windmill-home-$(id -u)"
HOME="/tmp/windmill-home-$(id -u)"
mkdir -p "$HOME"
fi
export HOME
# Register CA certificates mounted into the image before anything opens a TLS connection.
# Best-effort on purpose, unlike INIT_SCRIPT below: a non-root UID cannot write /etc/ssl/certs, and
# a deployment that never needed a custom CA must still boot. Env var names and the default-off
# behavior match the server/worker binary, so one setting covers every container. What the system
# trust store does and does not reach is documented in debugger/README.md.
CA_CERT_DIR=/usr/local/share/ca-certificates
update_ca_certificates() {
local reason="$1"
local tool="${RUN_UPDATE_CA_CERTIFICATE_PATH:-/usr/sbin/update-ca-certificates}"
local output
if [ ! -x "$tool" ]; then
echo "[entrypoint] $reason but $tool is not executable, skipping CA update"
return
fi
echo "[entrypoint] $reason, running $tool"
if output=$("$tool" 2>&1); then
echo "[entrypoint] CA certificates updated"
else
# Carry the tool's own message: the usual cause is an unwritable /etc/ssl/certs under a
# non-root UID, but guessing that in place of the real error hides everything else.
echo "[entrypoint] WARNING: $tool failed (UID $(id -u)): ${output:-no output}; continuing" >&2
fi
}
if [ "$(echo "${RUN_UPDATE_CA_CERTIFICATE_AT_START:-false}" | tr '[:upper:]' '[:lower:]')" = "true" ]; then
update_ca_certificates "RUN_UPDATE_CA_CERTIFICATE_AT_START=true"
elif [ -n "$(find -L "$CA_CERT_DIR" -type f -name '*.crt' -print -quit 2>/dev/null)" ]; then
# Certificates mounted there are unambiguous intent, and they do nothing until registered, so
# take the same action without making the operator also find the env var.
update_ca_certificates "Found certificates in $CA_CERT_DIR"
elif [ -n "$(ls -A "$CA_CERT_DIR" 2>/dev/null)" ]; then
# Reporting success over a mount update-ca-certificates ignores would be worse than saying
# nothing: .pem is the spelling people reach for, and only .crt is read.
echo "[entrypoint] WARNING: $CA_CERT_DIR has files but none named *.crt, the only extension" \
"update-ca-certificates reads; they will be ignored" >&2
fi
# INIT_SCRIPT is the documented hook for preparing the host before anything reaches the network
# (CA certificates, proxies, mounts), matching the worker's INIT_SCRIPT. It must therefore complete
# before any service starts, and a failure has to abort: services that come up with an unprepared
# trust store fail every TLS handshake instead, which is far harder to diagnose.
if [ -n "$INIT_SCRIPT" ]; then
echo "[entrypoint] Running INIT_SCRIPT..."
bash -c "$INIT_SCRIPT" || {
code=$?
echo "[entrypoint] ERROR: INIT_SCRIPT failed with exit code $code, aborting" >&2
exit "$code"
}
echo "[entrypoint] INIT_SCRIPT completed"
fi
# Setup NETRC if provided (for LSP)
if [ -n "$NETRC" ]; then
echo "$NETRC" > "$HOME/.netrc"
chmod 600 "$HOME/.netrc"
fi
# Setup cache directory for LSP (falls back to the image's world-writable
# XDG_CACHE_HOME=/pyls/.cache when $HOME/.cache isn't mounted)
if [ -d "$HOME/.cache" ]; then
export XDG_CACHE_HOME="$HOME/.cache"
cp -r /pyls/.cache "$HOME/.cache" 2>/dev/null || true
fi
# Setup Monaco temp directory for LSP
mkdir -p /tmp/monaco
if [ ! -f /tmp/monaco/go.mod ]; then
echo -e "module mymod\ngo 1.26" > /tmp/monaco/go.mod
fi
echo "[entrypoint] Starting Windmill Extra Services"
echo "[entrypoint] ENABLE_LSP=${ENABLE_LSP:-true}"
echo "[entrypoint] ENABLE_MULTIPLAYER=${ENABLE_MULTIPLAYER:-true}"
echo "[entrypoint] ENABLE_DEBUGGER=${ENABLE_DEBUGGER:-true}"
echo "[entrypoint] ENABLE_GATEWAY=${ENABLE_GATEWAY:-true}"
# Start LSP service
if [ "${ENABLE_LSP:-true}" = "true" ]; then
echo "[entrypoint] Starting LSP on port ${LSP_PORT:-3001}..."
cd /pyls
PORT=${LSP_PORT:-3001} python3 pyls_launcher.py &
PIDS+=($!)
echo "[entrypoint] LSP started (PID: ${PIDS[-1]})"
fi
# Start Multiplayer service (custom y-websocket with logging)
if [ "${ENABLE_MULTIPLAYER:-true}" = "true" ]; then
echo "[entrypoint] Starting Multiplayer on port ${MULTIPLAYER_PORT:-3002}..."
cd /multiplayer
PORT=${MULTIPLAYER_PORT:-3002} HOST=${HOST:-0.0.0.0} node server.mjs &
PIDS+=($!)
echo "[entrypoint] Multiplayer started (PID: ${PIDS[-1]})"
fi
# Start Debugger service
if [ "${ENABLE_DEBUGGER:-true}" = "true" ]; then
echo "[entrypoint] Starting Debugger on port ${DEBUGGER_PORT:-3003}..."
cd /debugger
# Build debugger arguments
DEBUGGER_ARGS="--host ${HOST:-0.0.0.0} --port ${DEBUGGER_PORT:-3003}"
DEBUGGER_ARGS="$DEBUGGER_ARGS --windmill /usr/local/bin/windmill"
# Enable nsjail if requested
if [ "${ENABLE_NSJAIL:-false}" = "true" ]; then
DEBUGGER_ARGS="$DEBUGGER_ARGS --nsjail --nsjail-config /debugger/nsjail.debug.config.proto"
fi
bun run dap_debug_service.ts $DEBUGGER_ARGS &
PIDS+=($!)
echo "[entrypoint] Debugger started (PID: ${PIDS[-1]})"
fi
# Start Gateway reverse proxy (routes /ws/*, /ws_mp/*, /ws_debug/* to the right service)
if [ "${ENABLE_GATEWAY:-true}" = "true" ]; then
echo "[entrypoint] Starting Gateway on port ${GATEWAY_PORT:-3000}..."
cd /multiplayer
PORT=${GATEWAY_PORT:-3000} node gateway.mjs &
PIDS+=($!)
echo "[entrypoint] Gateway started (PID: ${PIDS[-1]})"
fi
# Check if any services were started
if [ ${#PIDS[@]} -eq 0 ]; then
echo "[entrypoint] WARNING: No services enabled. Set ENABLE_LSP, ENABLE_MULTIPLAYER, or ENABLE_DEBUGGER to true."
echo "[entrypoint] Sleeping indefinitely..."
sleep infinity
fi
echo "[entrypoint] All enabled services started. Waiting..."
# Wait for any process to exit
wait -n "${PIDS[@]}" 2>/dev/null || true
# If one process exits, check which one and report
for i in "${!PIDS[@]}"; do
if ! kill -0 "${PIDS[$i]}" 2>/dev/null; then
echo "[entrypoint] Service (PID: ${PIDS[$i]}) has exited"
fi
done
# Keep running and wait for remaining processes
wait