-
-
Notifications
You must be signed in to change notification settings - Fork 114
feat: publish startup warnings for deployment checks #4739
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
| >&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)" | ||
|
|
@@ -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 | ||
| ;; | ||
| *) | ||
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
@@ -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) | ||
|
|
@@ -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 | ||
| } | ||
|
|
@@ -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 "$@" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When startup takes more than five minutes—for example, while Useful? React with 👍 / 👎.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
||
| 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 |
| 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" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When the shared data volume is reused by containers running different arbitrary UIDs, as supported for OpenShift-style deployments, the first
mkdir -pcreates.docker-startup-warningswith the normal0755mode 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/databefore creating the per-container directory.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
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.