Ctrl K
ComunidadGuías…

docker-entrypoint.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/bin/sh
# Topic Watch container entrypoint.
#
# Bind-mounted volumes (./data:/app/data) keep their HOST ownership, so a host
# user whose UID/GID is not 1000 cannot write /app/data and initialization
# fails with "permission denied". To stay portable, this entrypoint:
#
#   1. When started as root, aligns the runtime user with the host-provided
#      PUID/PGID (default 1000:1000), chowns /app/data to match, then drops
#      privileges with gosu so the app never runs as root.
#   2. When already started as a non-root user (e.g. compose `user:` override),
#      runs the command as-is without attempting any privileged operation.
#
# Idempotent: re-running adjusts the existing appuser/appgroup in place.
set -eu

PUID="${PUID:-1000}"
PGID="${PGID:-1000}"
DATA_DIR="/app/data"

# Resolve the directory that will hold the SQLite database. db_path is a
# first-class configurable (TOPIC_WATCH_DB_PATH); it defaults to a relative
# path under /app/data, but an absolute or relocated value can point outside
# the bind-mounted volume. Such a directory is NOT chowned/probed by the
# /app/data logic below, so the dropped appuser would crash in init_db with
# "permission denied" (OVH-121). Surface it as its own path to handle.
DEFAULT_DB_PATH="data/topic_watch.db"
db_path="${TOPIC_WATCH_DB_PATH:-$DEFAULT_DB_PATH}"
case "$db_path" in
    /*) db_dir="$(dirname "$db_path")" ;;        # absolute → use as-is
    *)  db_dir="/app/$(dirname "$db_path")" ;;   # relative → resolved from /app
esac
db_basename="$(basename "$db_path")"
# When the DB dir already lives inside the data volume, the /app/data handling
# covers it — collapse to DATA_DIR so the common case touches a single path.
case "$db_dir/" in
    "$DATA_DIR"/* | "$DATA_DIR/") db_dir="$DATA_DIR" ;;
esac

# Validate that PUID and PGID are numeric.
case "$PUID" in
    *[!0-9]*) echo "ERROR: PUID must be a numeric user ID (got: '$PUID')" >&2; exit 1;;
esac
case "$PGID" in
    *[!0-9]*) echo "ERROR: PGID must be a numeric group ID (got: '$PGID')" >&2; exit 1;;
esac

# Warn loudly when asked to run as root (privilege drop would be a no-op).
if [ "$PUID" = "0" ]; then
    echo "WARNING: PUID=0 — the app will run as root, which defeats privilege drop." >&2
fi

# Resolve a directory to its real, symlink-free absolute path. Requires the
# directory to already exist. Prints nothing and returns non-zero if it
# cannot be resolved (missing, or not traversable).
canon_dir() {
    ( cd "$1" 2>/dev/null && pwd -P )
}

# AUG-007: a relocated TOPIC_WATCH_DB_PATH can resolve its parent to the
# filesystem root or another broad system directory — e.g. "/topic_watch.db"
# resolves to "/", and "../topic_watch.db" resolves to the directory above
# /app. Refuse to treat any of these as a database directory at all, so
# neither the chown nor the writability probe below ever touches them.
is_forbidden_chown_target() {
    case "$1" in
        ""|/) return 0 ;;
        /bin|/boot|/dev|/etc|/home|/lib|/lib64|/media|/mnt|/opt|/proc|/root|/run|/sbin|/srv|/sys|/tmp|/usr|/var) return 0 ;;
        *) return 1 ;;
    esac
}

if [ "$db_dir" != "$DATA_DIR" ]; then
    mkdir -p "$db_dir" 2>/dev/null || true
    resolved_db_dir="$(canon_dir "$db_dir" 2>/dev/null || true)"
    if [ -n "$resolved_db_dir" ]; then
        if is_forbidden_chown_target "$resolved_db_dir"; then
            echo "ERROR: TOPIC_WATCH_DB_PATH ('$db_path') resolves its directory to" >&2
            echo "       '$resolved_db_dir', a filesystem root or system directory." >&2
            echo "       Refusing to touch it. Point TOPIC_WATCH_DB_PATH at a dedicated" >&2
            echo "       subdirectory (e.g. a mounted volume) instead." >&2
            exit 1
        fi
        db_dir="$resolved_db_dir"
    fi
    # else: directory could not be resolved yet (e.g. not writable as a
    # non-root user) — fall through and let the probe/chown below produce
    # their own actionable error.
fi

# Probe one directory for writability by the current user, failing with an
# actionable message instead of crashing deep inside init_db. Used on the
# non-root path, where we cannot chown to fix ownership ourselves.
probe_writable() {
    probe_dir="$1"
    mkdir -p "$probe_dir" 2>/dev/null || true
    if { touch "$probe_dir/.wtest" 2>/dev/null && rm -f "$probe_dir/.wtest"; }; then
        return 0
    fi
    cur_uid="$(id -u)"
    cur_gid="$(id -g)"
    dir_owner_uid="$(stat -c %u "$probe_dir" 2>/dev/null || echo '?')"
    dir_owner_gid="$(stat -c %g "$probe_dir" 2>/dev/null || echo '?')"
    echo "ERROR: $probe_dir is not writable by uid=$cur_uid gid=$cur_gid" >&2
    echo "       (the directory is owned by uid=$dir_owner_uid gid=$dir_owner_gid)." >&2
    echo "       The container was started as a non-root user (e.g. a compose 'user:' override)," >&2
    echo "       so it cannot fix volume ownership itself. Remediate on the host with:" >&2
    echo "         chown -R $cur_uid:$cur_gid <host path mounted at $probe_dir>" >&2
    echo "       or drop the 'user:' override and set PUID/PGID to match the host owner." >&2
    return 1
}

# If we are not root, we cannot chown or change identity. Verify the data
# volume — and the DB directory when it lives outside the volume — are writable
# by the current (non-root) user. This is a diagnostic, not a privileged op
# (which couldn't run as non-root anyway).
if [ "$(id -u)" != "0" ]; then
    probe_writable "$DATA_DIR" || exit 1
    if [ "$db_dir" != "$DATA_DIR" ]; then
        probe_writable "$db_dir" || exit 1
    fi
    exec "$@"
fi

# Align the appgroup GID with PGID (idempotent).
current_gid="$(getent group appgroup | cut -d: -f3 || true)"
if [ "$current_gid" != "$PGID" ]; then
    groupmod -o -g "$PGID" appgroup
fi

# Align the appuser UID with PUID (idempotent).
current_uid="$(id -u appuser 2>/dev/null || true)"
if [ "$current_uid" != "$PUID" ]; then
    usermod -o -u "$PUID" appuser
fi

# Ensure a directory exists and is owned by the runtime user, recursively.
# Skip the recursive walk when directory-level ownership already matches — it
# is expensive on large bind-mounted volumes and unnecessary when the UID/GID
# haven't changed. Only ever called on DATA_DIR or a known-bounded
# subdirectory of it (never on an arbitrary configured db_dir — AUG-007).
chown_dir_recursive() {
    target_dir="$1"
    mkdir -p "$target_dir"
    dir_uid="$(stat -c %u "$target_dir")"
    dir_gid="$(stat -c %g "$target_dir")"
    if [ "$dir_uid" != "$PUID" ] || [ "$dir_gid" != "$PGID" ]; then
        chown -R "$PUID:$PGID" "$target_dir"
    fi
}

# Chown a single existing path (file or directory entry) if its ownership
# doesn't already match. Never recursive, so it is safe to call on a path
# inside an arbitrary configured directory.
chown_path_if_needed() {
    p="$1"
    [ -e "$p" ] || return 0
    p_uid="$(stat -c %u "$p")"
    p_gid="$(stat -c %g "$p")"
    if [ "$p_uid" != "$PUID" ] || [ "$p_gid" != "$PGID" ]; then
        chown "$PUID:$PGID" "$p"
    fi
}

# Repair ownership of the exact database + sidecar files under a directory,
# regardless of whether the containing directory's own ownership already
# matches (AUG-057: a directory created with the right owner can still hold
# root-owned files copied/restored in afterwards). Never recursive.
chown_db_files() {
    dir="$1"
    base="$2"
    chown_path_if_needed "$dir/$base"
    chown_path_if_needed "$dir/$base-wal"
    chown_path_if_needed "$dir/$base-shm"
    chown_path_if_needed "$dir/$base-journal"
}

# Ensure the data volume itself is writable by the runtime user, and that the
# config file and database (+ sidecars) it holds are individually correctly
# owned even when the directory ownership already matched (AUG-057).
chown_dir_recursive "$DATA_DIR"
chown_path_if_needed "$DATA_DIR/config.yml"
if [ "$db_dir" = "$DATA_DIR" ]; then
    chown_db_files "$DATA_DIR" "$db_basename"
else
    # When the DB lives outside the data volume (absolute/relocated db_path),
    # its parent is not covered above and would stay root-owned (OVH-121). Fix
    # it WITHOUT recursing an arbitrary configured directory (AUG-007): chown
    # only the directory entry itself and the exact database + sidecar files.
    chown_path_if_needed "$db_dir"
    chown_db_files "$db_dir" "$db_basename"
fi
# Backups live alongside the database file (app/database.py _backup_db) and
# are capped at 5 files by the app itself, so recursing just this
# app-managed subdirectory is cheap and safe regardless of where the
# database lives.
if [ -d "$db_dir/backups" ]; then
    chown_dir_recursive "$db_dir/backups"
fi

# Drop privileges and run the app as the (now host-aligned) appuser.
exec gosu "$PUID:$PGID" "$@"