Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/container-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ jobs:
- name: Test Celery worker mode validation
if: inputs.test == 'celery-single'
run: python3 tests/integration/test-celery-mode.py
- name: Test startup warning heartbeat
run: ./tests/integration/test-warning-heartbeat
- name: Test content
run: ./tests/integration/test-content
- name: Generate configuration
Expand Down Expand Up @@ -96,7 +98,9 @@ jobs:
run: ../tests/integration/test-inspect
- name: Run Django Checks
working-directory: docker-compose
run: ../tests/integration/test-checks
run: ../tests/integration/test-checks "$WEBLATE_TEST"
env:
WEBLATE_TEST: ${{ inputs.test }}
- name: Verify supervisor
working-directory: docker-compose
run: ../tests/integration/test-supervisor
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5m CMD ["/app/bin/health_

# Use Docker specific settings
ENV DJANGO_SETTINGS_MODULE=weblate.settings_docker
ENV WEBLATE_DOCKER_CONTAINER=1

# Copy built environment
COPY --from=build /app /app
Expand Down Expand Up @@ -110,7 +111,7 @@ RUN rm -f /etc/localtime /etc/timezone \
&& sed -i '/pam_rootok.so/a auth requisite pam_deny.so' /etc/pam.d/su

# Entrypoint
COPY --link --chmod=0755 start health_check /app/bin/
COPY --link --chmod=0755 start health_check docker_warning_heartbeat /app/bin/

EXPOSE 8080
VOLUME /app/data
Expand Down
47 changes: 47 additions & 0 deletions docker_warning_heartbeat
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/bin/sh
set -eu

report=${WEBLATE_DOCKER_WARNING_REPORT:-}
hostname=${WEBLATE_DOCKER_WARNING_HOSTNAME:-unknown}
service=${WEBLATE_DOCKER_WARNING_SERVICE:-all}
warnings=${WEBLATE_DOCKER_WARNING_MESSAGES:-}
sleep_pid=
if [ -z "$report" ]; then
exit 0
fi

initialize_report() {
mkdir -p "$report"
printf '%s\n' "$hostname" > "$report/hostname"
printf '%s\n' "$service" > "$report/service"
printf '%s' "$warnings" > "$report/warnings"
touch "$report/heartbeat" "$report"
}

cleanup_report() {
if [ -n "$sleep_pid" ]; then
kill "$sleep_pid" 2> /dev/null || true
fi
rm -f -- \
"$report/heartbeat" \
"$report/hostname" \
"$report/service" \
"$report/warnings" || true
rmdir -- "$report" 2> /dev/null || true
}

shutdown() {
cleanup_report
exit 0
}

trap shutdown HUP INT TERM
initialize_report

while :; do
touch "$report/heartbeat" "$report"
sleep 60 &
sleep_pid=$!
wait "$sleep_pid"
sleep_pid=
done
9 changes: 9 additions & 0 deletions etc/supervisor/conf.d/docker-warning-heartbeat.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[program:docker-warning-heartbeat]
command = /app/bin/docker_warning_heartbeat
autorestart = true
stopasgroup = true
killasgroup = true
stdout_logfile=/dev/fd/1
stdout_logfile_maxbytes=0
redirect_stderr=true
priority = 50
109 changes: 100 additions & 9 deletions start
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,88 @@ initialize_env_defaults() {
export CLIENT_MAX_BODY_SIZE
}

startup_warning() {
local message=$1

message=${message//$'\n'/ }
message=${message//$'\r'/ }
>&2 echo "Warning: $message"
if [[ -n ${WEBLATE_DOCKER_WARNING_MESSAGES:-} ]]; then
WEBLATE_DOCKER_WARNING_MESSAGES+=$'\n'
fi
WEBLATE_DOCKER_WARNING_MESSAGES+="$message"
export WEBLATE_DOCKER_WARNING_MESSAGES
if [[ -n ${WEBLATE_DOCKER_WARNING_REPORT:-} ]]; then
printf '%s\n' "$message" >> "$WEBLATE_DOCKER_WARNING_REPORT/warnings" || true
fi
}

cleanup_startup_warning_report() {
local report=${WEBLATE_DOCKER_WARNING_REPORT:-}

if [[ -z $report ]]; then
return 0
fi
rm -f -- \
"$report/heartbeat" \
"$report/hostname" \
"$report/service" \
"$report/warnings" || true
rmdir -- "$report" 2> /dev/null || true
WEBLATE_DOCKER_WARNING_REPORT=
}

initialize_startup_warning_report() {
local command=${1:-}
local data_dir=${WEBLATE_DATA_DIR:-/app/data}
local report_id
local warning_root="$data_dir/.docker-startup-warnings"

if [[ ${WEBLATE_DOCKER_CONTAINER:-} != 1 || $command != "runserver" ]]; then
return 0
fi

report_id=$(cat /proc/sys/kernel/random/uuid)
WEBLATE_DOCKER_WARNING_REPORT="$warning_root/$report_id"
WEBLATE_DOCKER_WARNING_HOSTNAME=${HOSTNAME:-unknown}
WEBLATE_DOCKER_WARNING_SERVICE=${WEBLATE_SERVICE:-all}
WEBLATE_DOCKER_WARNING_MESSAGES=
if ! mkdir -p "$WEBLATE_DOCKER_WARNING_REPORT"; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Make the shared warning root group-writable

When the shared data volume is reused by containers running different arbitrary UIDs, as supported for OpenShift-style deployments, the first mkdir -p creates .docker-startup-warnings with the normal 0755 mode and ownership of that container's UID. A later container with another UID but the shared root group cannot create its report beneath that directory, so this path prints the initialization error and silently disables warning publication and stale-report cleanup. Create the shared root with group-write permissions consistent with /app/data before creating the per-container directory.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we actually support such setups.

>&2 echo "Could not initialize Docker startup warning report."
WEBLATE_DOCKER_WARNING_REPORT=
return 0
fi
printf '%s\n' \
"Signature: 8a477f597d28d172789f06886806bc55d" \
"# Docker startup warning reports are ephemeral." \
> "$warning_root/CACHEDIR.TAG" || true
if ! {
printf '%s\n' "$WEBLATE_DOCKER_WARNING_HOSTNAME" > "$WEBLATE_DOCKER_WARNING_REPORT/hostname" &&
printf '%s\n' "$WEBLATE_DOCKER_WARNING_SERVICE" > "$WEBLATE_DOCKER_WARNING_REPORT/service" &&
: > "$WEBLATE_DOCKER_WARNING_REPORT/warnings" &&
touch "$WEBLATE_DOCKER_WARNING_REPORT/heartbeat" "$WEBLATE_DOCKER_WARNING_REPORT"
}; then
>&2 echo "Could not write Docker startup warning report."
cleanup_startup_warning_report
return 0
fi
export \
WEBLATE_DOCKER_WARNING_HOSTNAME \
WEBLATE_DOCKER_WARNING_MESSAGES \
WEBLATE_DOCKER_WARNING_REPORT \
WEBLATE_DOCKER_WARNING_SERVICE
trap cleanup_startup_warning_report EXIT

# Reports stop being displayed after five minutes. Remove expired ones
# here to keep the shared data directory bounded after abrupt shutdowns.
find "$warning_root" \
-mindepth 1 \
-maxdepth 1 \
-type d \
-mmin +5 \
-exec rm -rf -- {} + 2> /dev/null || true
}

ensure_data_volume_writable() {
if [[ ! -w /app/data ]]; then
echo "The /app/data volume is not writable, please adjust the permissions. Weblate is running as uid $(id -u)"
Expand Down Expand Up @@ -216,7 +298,7 @@ ensure_runtime_user() {

case "${USER_NAME:-weblate}" in
*[!A-Za-z0-9._-]* | "")
echo "Ignoring invalid USER_NAME value; using fallback username 'weblate'."
startup_warning "Ignoring invalid USER_NAME value; using fallback username 'weblate'."
nss_wrapper_user_name=weblate
;;
*)
Expand Down Expand Up @@ -393,12 +475,12 @@ resolve_celery_worker_mode() {
if [[ -z ${CELERY_WORKER_MODE:-} ]]; then
if [[ ${CELERY_SINGLE_PROCESS:-0} == 1 ]]; then
CELERY_WORKER_MODE=single
>&2 echo "Warning: CELERY_SINGLE_PROCESS is deprecated; use CELERY_WORKER_MODE=single instead."
startup_warning "CELERY_SINGLE_PROCESS is deprecated; use CELERY_WORKER_MODE=single instead."
else
CELERY_WORKER_MODE=combined
fi
elif [[ ${CELERY_SINGLE_PROCESS:-0} == 1 ]]; then
>&2 echo "Warning: CELERY_SINGLE_PROCESS is deprecated and ignored because CELERY_WORKER_MODE is set; remove CELERY_SINGLE_PROCESS."
startup_warning "CELERY_SINGLE_PROCESS is deprecated and ignored because CELERY_WORKER_MODE is set; remove CELERY_SINGLE_PROCESS."
fi

case $CELERY_WORKER_MODE in
Expand All @@ -424,7 +506,7 @@ resolve_celery_worker_mode() {

if ((${#split_options[@]})); then
mode_options="CELERY_${CELERY_WORKER_MODE^^}_OPTIONS"
>&2 echo "Warning: CELERY_WORKER_MODE=$CELERY_WORKER_MODE ignores split worker options: ${split_options[*]}; use $mode_options or CELERY_WORKER_MODE=split."
startup_warning "CELERY_WORKER_MODE=$CELERY_WORKER_MODE ignores split worker options: ${split_options[*]}; use $mode_options or CELERY_WORKER_MODE=split."
fi

export CELERY_WORKER_MODE
Expand All @@ -443,13 +525,21 @@ prepare_supervisor_services() {
# Remove possible stale files from previous start.
rm -f "$SUPERVISOR_CONF"/*

if [[ -n ${WEBLATE_DOCKER_WARNING_REPORT:-} ]]; then
ln -s /etc/supervisor/conf.d/docker-warning-heartbeat.conf "$SUPERVISOR_CONF"
fi

if [[ -n $WEBLATE_SERVICE ]]; then
ln -s "/etc/supervisor/conf.d/$WEBLATE_SERVICE.conf" "$SUPERVISOR_CONF"
return 0
fi

# Symlink all non-celery services.
find /etc/supervisor/conf.d -type f ! -name 'celery-*.conf' -exec ln -s -t "$SUPERVISOR_CONF" {} +
find /etc/supervisor/conf.d \
-type f \
! -name 'celery-*.conf' \
! -name 'docker-warning-heartbeat.conf' \
-exec ln -s -t "$SUPERVISOR_CONF" {} +

case $CELERY_WORKER_MODE in
combined)
Expand Down Expand Up @@ -540,7 +630,7 @@ configure_forwarded_for() {
fi

if [[ -n ${WEBLATE_IP_PROXY_OFFSET:-} && $WEBLATE_IP_PROXY_OFFSET != 0 ]]; then
echo "Ignoring WEBLATE_IP_PROXY_OFFSET=$WEBLATE_IP_PROXY_OFFSET: the built-in nginx normalizes X-Forwarded-For to one address."
startup_warning "Ignoring WEBLATE_IP_PROXY_OFFSET=$WEBLATE_IP_PROXY_OFFSET: the built-in nginx normalizes X-Forwarded-For to one address."
fi
export WEBLATE_IP_PROXY_OFFSET=0
}
Expand Down Expand Up @@ -758,13 +848,14 @@ runserver_main() {
main() {
load_secret_env_files
initialize_env_defaults
if [[ ${1:-} == "runserver" && -z ${WEBLATE_SERVICE:-} ]]; then
resolve_celery_worker_mode
fi

echo "Starting Weblate $WEBLATE_VERSION..."

prepare_runtime_files
initialize_startup_warning_report "$@"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Start the heartbeat before long-running startup work

When startup takes more than five minutes—for example, while run_startup_maintenance performs a large migration—this initializes the only heartbeat before the warning-producing checks, but the heartbeat process is not started until start_supervisord at the end of runserver_main. The report therefore becomes stale and stops appearing in deployment checks during the slow startup, and another container's expiry cleanup can remove it. Start the helper earlier or refresh the heartbeat throughout pre-supervisor startup.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not a big deal, in most cases the long startup is in the single container, so there will be nothing consuming the checks.

if [[ ${1:-} == "runserver" && -z ${WEBLATE_SERVICE:-} ]]; then
resolve_celery_worker_mode
fi
ensure_runtime_user
require_site_domain
export_runtime_env
Expand Down
32 changes: 30 additions & 2 deletions tests/integration/test-checks
Original file line number Diff line number Diff line change
@@ -1,4 +1,32 @@
#!/bin/sh
docker compose exec -T \

OUTPUT=$(docker compose exec -T \
--user weblate \
weblate weblate check --deploy --fail-level WARNING
weblate weblate check --deploy --fail-level WARNING 2>&1)
RESULT=$?

SUPPORTS_DOCKER_WARNINGS=0
if docker compose exec -T \
--user weblate \
weblate /app/venv/bin/python -c \
'from weblate.utils.checks import DOC_LINKS; raise SystemExit("weblate.W049" not in DOC_LINKS)'; then
SUPPORTS_DOCKER_WARNINGS=1
fi

if [ "${1:-}" = "celery-single-legacy" ] && [ "$SUPPORTS_DOCKER_WARNINGS" -eq 1 ]; then
if [ "$RESULT" -eq 0 ]; then
echo "Docker startup warning did not affect the deployment check" >&2
exit 1
fi
if ! echo "$OUTPUT" | grep -F "weblate.W049" > /dev/null; then
echo "$OUTPUT" >&2
exit 1
fi
if ! echo "$OUTPUT" | grep -F "CELERY_SINGLE_PROCESS is deprecated" > /dev/null; then
echo "$OUTPUT" >&2
exit 1
fi
elif [ "$RESULT" -ne 0 ]; then
echo "$OUTPUT" >&2
exit "$RESULT"
fi
61 changes: 61 additions & 0 deletions tests/integration/test-warning-heartbeat
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/bin/sh
set -eu

SCRIPT_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
DOCKER_ROOT=$(CDPATH='' cd -- "$SCRIPT_DIR/../.." && pwd)
TEST_ROOT=$(mktemp -d)
REPORT="$TEST_ROOT/report"

run_heartbeat() {
WEBLATE_DOCKER_WARNING_HOSTNAME=test-host \
WEBLATE_DOCKER_WARNING_MESSAGES="Test warning" \
WEBLATE_DOCKER_WARNING_REPORT="$REPORT" \
WEBLATE_DOCKER_WARNING_SERVICE=test-service \
"$DOCKER_ROOT/docker_warning_heartbeat" &
HEARTBEAT_PID=$!
}

wait_for_heartbeat() {
ATTEMPTS=0
while [ ! -f "$REPORT/heartbeat" ]; do
if ! kill -0 "$HEARTBEAT_PID" 2> /dev/null; then
wait "$HEARTBEAT_PID"
echo "Docker startup warning heartbeat exited before initialization" >&2
exit 1
fi
ATTEMPTS=$((ATTEMPTS + 1))
if [ "$ATTEMPTS" -ge 50 ]; then
kill -TERM "$HEARTBEAT_PID"
wait "$HEARTBEAT_PID"
echo "Docker startup warning heartbeat was not initialized" >&2
exit 1
fi
sleep 0.1
done
}

stop_heartbeat() {
kill -TERM "$HEARTBEAT_PID"
wait "$HEARTBEAT_PID"
}

assert_report_removed() {
if [ -d "$REPORT" ]; then
echo "Docker startup warning report was not removed on shutdown" >&2
exit 1
fi
}

run_heartbeat
wait_for_heartbeat
grep -Fx "Test warning" "$REPORT/warnings"
stop_heartbeat
assert_report_removed

# Supervisord can restart the heartbeat helper without rerunning the entrypoint.
run_heartbeat
wait_for_heartbeat
grep -Fx "Test warning" "$REPORT/warnings"
stop_heartbeat
assert_report_removed
rmdir "$TEST_ROOT"
Loading