mirror of
https://github.com/TencentCloud/Octop.git
synced 2026-10-20 06:19:17 +08:00
The entrypoint guard only looked for ~/.octop/octop.db, which PostgreSQL (and other external) backends never create in the data volume, so every container restart re-ran `octop init`. The unconditional admin INSERT then aborted the container with UniqueViolation on users_username_key. - docker/docker-entrypoint.sh: write a backend-agnostic marker (~/.octop/.initialized) after a successful `octop init`; skip the bootstrap block on restarts when the marker or octop.db exists. When init reports the admin already exists (upgrade of a pre-marker deployment), keep the existing credential card instead of writing a password that was never applied; also mention external DBs in the generic init-failure hint. - src/octop/cli/commands/init.py: make the admin bootstrap idempotent — when the username already exists, skip creation with an explicit message (the entrypoint greps for it) instead of hitting UNIQUE(username). Tests: entrypoint tests run the real script with a stub `octop` on PATH (first boot + restart for an external backend, sqlite regression guard, credential preservation, failed-init retry); a CLI test covers rerunning `octop init` when the DB lives outside ~/.octop. Fixes #1866 Signed-off-by: fengyue-xve <1759519137@qq.com>
223 lines
8.5 KiB
Bash
Executable File
223 lines
8.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# =============================================================================
|
|
# Octop 容器入口脚本
|
|
#
|
|
# 环境变量:
|
|
# HOME — 必须为 /data,使 ~/.octop 映射到数据卷
|
|
# OCTOP_DEFAULT_PASSWORD — 首次管理员密码(须 ≥8 位且含字母和数字)。
|
|
# 合格则 octop init 建账号;未设置或未通过
|
|
# 策略则不建管理员,启动后走 Web 设置向导。
|
|
# OCTOP_ADMIN_USERNAME — 首次管理员用户名(默认: admin)
|
|
# OCTOP_ADMIN_DISPLAY_NAME — 可选显示名
|
|
# OCTOP_PORT — 服务端口(默认: 8088)
|
|
#
|
|
# 首次引导(issue #502):容器不得因弱默认密码重启循环。未设置或不合格
|
|
# 的 OCTOP_DEFAULT_PASSWORD 不再静默换成随机管理员密码,而是把向导口令
|
|
# 写到数据卷(/data/.octop/octop-login.txt),打开页面走设置向导。
|
|
#
|
|
# 初始化标记(issue #1866):PostgreSQL 等外部数据库不会在数据卷生成
|
|
# octop.db,仅凭 DB_FILE 判断会在每次重启重复执行 octop init(管理员
|
|
# INSERT 撞 UNIQUE 约束直接崩溃)。octop init 成功后写入
|
|
# /data/.octop/.initialized,重启命中该标记即跳过引导直接启动。
|
|
# =============================================================================
|
|
set -euo pipefail
|
|
|
|
export HOME="${HOME:-/data}"
|
|
OCTOP_HOME="${OCTOP_HOME:-${HOME}/.octop}"
|
|
export OCTOP_HOME
|
|
DB_FILE="${OCTOP_HOME}/octop.db"
|
|
INIT_MARKER="${OCTOP_HOME}/.initialized"
|
|
CREDENTIAL_FILE="${OCTOP_HOME}/credential.txt"
|
|
WIZARD_FILE="${OCTOP_HOME}/octop-login.txt"
|
|
ADMIN_USERNAME="${OCTOP_ADMIN_USERNAME:-admin}"
|
|
ADMIN_DISPLAY_NAME="${OCTOP_ADMIN_DISPLAY_NAME:-Admin}"
|
|
PORT="${OCTOP_PORT:-8088}"
|
|
|
|
# 与 src/octop/infra/users/password.py 的 _COMMON_PASSWORDS 保持一致。
|
|
_OCTOP_COMMON_PASSWORDS="password password1 password12 password123 12345678 123456789 qwerty123 admin123 welcome1 letmein1 changeme1 octop123 abc12345 iloveyou1"
|
|
|
|
# 生成随机口令(首字符字母、末字符数字,避开易混淆字符)。
|
|
octop_random_password() {
|
|
local letters='abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ'
|
|
local digits='23456789'
|
|
local all="${letters}${digits}" n=16 out="" i b
|
|
b="$(od -An -N1 -tu1 /dev/urandom 2>/dev/null | tr -d '[:space:]')"
|
|
out="${letters:$((b % ${#letters})):1}"
|
|
for ((i = 1; i < n - 1; i++)); do
|
|
b="$(od -An -N1 -tu1 /dev/urandom 2>/dev/null | tr -d '[:space:]')"
|
|
out+="${all:$((b % ${#all})):1}"
|
|
done
|
|
b="$(od -An -N1 -tu1 /dev/urandom 2>/dev/null | tr -d '[:space:]')"
|
|
out+="${digits:$((b % ${#digits})):1}"
|
|
printf '%s' "$out"
|
|
}
|
|
|
|
# 校验是否满足应用侧密码策略。失败时向 stderr 输出原因,返回非零。
|
|
octop_validate_password() {
|
|
local pw="$1" lower
|
|
if [ -z "$pw" ]; then
|
|
echo "密码为空" >&2
|
|
return 1
|
|
fi
|
|
if [ "${#pw}" -lt 8 ]; then
|
|
echo "密码长度至少 8 位" >&2
|
|
return 1
|
|
fi
|
|
if ! printf '%s' "$pw" | grep -q '[A-Za-z]'; then
|
|
echo "密码必须同时包含字母和数字" >&2
|
|
return 1
|
|
fi
|
|
if ! printf '%s' "$pw" | grep -q '[0-9]'; then
|
|
echo "密码必须同时包含字母和数字" >&2
|
|
return 1
|
|
fi
|
|
lower="$(printf '%s' "$pw" | tr 'A-Z' 'a-z')"
|
|
case " ${_OCTOP_COMMON_PASSWORDS} " in
|
|
*" ${lower} "*)
|
|
echo "密码过于常见" >&2
|
|
return 1
|
|
;;
|
|
esac
|
|
return 0
|
|
}
|
|
|
|
# 向导口令写到数据卷,并让 $HOME/octop-login.txt 指向同一文件(服务端读 HOME)。
|
|
octop_seed_wizard_password() {
|
|
mkdir -p "$OCTOP_HOME" "$HOME"
|
|
if [ ! -s "$WIZARD_FILE" ]; then
|
|
octop_random_password > "$WIZARD_FILE"
|
|
chmod 600 "$WIZARD_FILE"
|
|
fi
|
|
local home_file="${HOME}/octop-login.txt"
|
|
if [ "$home_file" = "$WIZARD_FILE" ]; then
|
|
return 0
|
|
fi
|
|
if [ -f "$home_file" ] && [ ! -L "$home_file" ]; then
|
|
cp -f "$home_file" "$WIZARD_FILE"
|
|
chmod 600 "$WIZARD_FILE"
|
|
return 0
|
|
fi
|
|
ln -sfn "$WIZARD_FILE" "$home_file"
|
|
}
|
|
|
|
octop_write_setup_hint() {
|
|
local pw
|
|
pw="$(tr -d '\n\r' < "$WIZARD_FILE")"
|
|
cat > "$CREDENTIAL_FILE" << EOF
|
|
Octop Setup Required
|
|
====================
|
|
URL: http://<host>:${PORT}
|
|
|
|
No admin account has been created yet.
|
|
Open the dashboard and complete the setup wizard.
|
|
|
|
Wizard password: ${pw}
|
|
|
|
This password only unlocks the wizard (same value as octop-login.txt).
|
|
Choose the admin username and password in the browser.
|
|
EOF
|
|
chmod 600 "$CREDENTIAL_FILE"
|
|
}
|
|
|
|
octop_write_admin_credential() {
|
|
local password="$1"
|
|
cat > "$CREDENTIAL_FILE" << EOF
|
|
Octop Login Credential
|
|
======================
|
|
URL: http://<host>:${PORT}
|
|
Username: ${ADMIN_USERNAME}
|
|
Password: ${password}
|
|
|
|
Please change this password after first login!
|
|
- Via Web: avatar menu → Change password
|
|
- Via CLI: docker exec -it <container> octop user passwd --username $ADMIN_USERNAME
|
|
|
|
This file is rewritten whenever the initial password is (re)generated here.
|
|
If you changed the password inside the Web console, that password wins.
|
|
EOF
|
|
chmod 600 "$CREDENTIAL_FILE"
|
|
}
|
|
|
|
octop_start_setup_wizard() {
|
|
echo "[entrypoint] 将启动设置向导。向导口令已写入: $WIZARD_FILE"
|
|
echo "[entrypoint] 打开 http://<host>:${PORT} 完成初始设置。"
|
|
octop_seed_wizard_password
|
|
octop_write_setup_hint
|
|
}
|
|
|
|
octop_run_init() {
|
|
local password="$1" init_log="$2"
|
|
octop init \
|
|
--yes \
|
|
--admin-username "$ADMIN_USERNAME" \
|
|
--admin-password "$password" \
|
|
${ADMIN_DISPLAY_NAME:+--admin-display-name "$ADMIN_DISPLAY_NAME"} \
|
|
>"$init_log" 2>&1
|
|
}
|
|
|
|
octop_entrypoint_main() {
|
|
local default_password="${OCTOP_DEFAULT_PASSWORD:-}"
|
|
local init_log reason
|
|
|
|
if [ ! -f "$DB_FILE" ] && [ ! -f "$INIT_MARKER" ]; then
|
|
echo "[entrypoint] 首次启动,正在初始化 Octop..."
|
|
mkdir -p "$OCTOP_HOME"
|
|
|
|
if [ -n "$default_password" ] && octop_validate_password "$default_password" 2>/dev/null; then
|
|
init_log="$(mktemp)"
|
|
if octop_run_init "$default_password" "$init_log"; then
|
|
# init 对已有管理员幂等(输出 "already exists, skipping",见
|
|
# cli/commands/init.py)。这种情况说明数据库早已完成引导,
|
|
# 重写凭据文件只会写下一个从未生效的密码,故保持原文件。
|
|
if grep -q 'already exists, skipping' "$init_log"; then
|
|
echo "[entrypoint] 数据库已完成引导(管理员已存在),保留现有凭据文件。"
|
|
else
|
|
octop_write_admin_credential "$default_password"
|
|
echo "[entrypoint] 凭据已保存至: $CREDENTIAL_FILE"
|
|
fi
|
|
rm -f "$init_log"
|
|
touch "$INIT_MARKER"
|
|
echo "[entrypoint] 已写入初始化标记 ${INIT_MARKER},重启将跳过 octop init。"
|
|
elif grep -qiE 'password is too common|password too short|password must include' "$init_log"; then
|
|
cat "$init_log" >&2 || true
|
|
rm -f "$init_log"
|
|
echo "[entrypoint] 指定的初始密码被应用拒绝,改为启动设置向导。"
|
|
octop_start_setup_wizard
|
|
else
|
|
cat "$init_log" >&2 || true
|
|
rm -f "$init_log"
|
|
echo "[entrypoint] 初始化失败(不是密码策略问题)。若数据目录已有文件但没有 octop.db(或使用外部数据库后端),请检查卷挂载与数据库连接。" >&2
|
|
exit 1
|
|
fi
|
|
else
|
|
if [ -z "$default_password" ]; then
|
|
echo "[entrypoint] 未设置 OCTOP_DEFAULT_PASSWORD,不自动创建管理员。"
|
|
else
|
|
reason="$(octop_validate_password "$default_password" 2>&1 || true)"
|
|
echo "[entrypoint] OCTOP_DEFAULT_PASSWORD 未通过密码策略(${reason:-过弱或过于常见}),不自动创建管理员。"
|
|
fi
|
|
octop_start_setup_wizard
|
|
fi
|
|
fi
|
|
|
|
if [ $# -eq 0 ]; then
|
|
echo "[entrypoint] 正在启动 Octop,端口 $PORT..."
|
|
exec octop run --host 0.0.0.0 --port "$PORT"
|
|
fi
|
|
|
|
if [ "$1" = "octop" ]; then
|
|
echo "[entrypoint] 执行命令: $*"
|
|
exec "$@"
|
|
fi
|
|
|
|
echo "[entrypoint] 执行命令: $*"
|
|
exec "$@"
|
|
}
|
|
|
|
# Allow tests to source helpers without starting the server.
|
|
if [ "${OCTOP_ENTRYPOINT_LIB:-}" = "1" ]; then
|
|
return 0 2>/dev/null || exit 0
|
|
fi
|
|
|
|
octop_entrypoint_main "$@"
|