215 lines
8.1 KiB
Bash
215 lines
8.1 KiB
Bash
#!/bin/sh
|
|
set -eu
|
|
|
|
action="${1:-}"
|
|
project="$(pwd)"
|
|
test -f "$project/compose.yaml" || { echo '找不到 compose.yaml'; exit 1; }
|
|
|
|
update_env() {
|
|
key="$1"
|
|
value="$2"
|
|
if grep -q "^${key}=" .env; then
|
|
sed -i "s|^${key}=.*|${key}=${value}|" .env
|
|
else
|
|
printf '%s=%s\n' "$key" "$value" >> .env
|
|
fi
|
|
}
|
|
|
|
wait_for_container() {
|
|
name="$1"
|
|
expected="$2"
|
|
timeout="$3"
|
|
started_at="$(date +%s)"
|
|
last_state='missing'
|
|
last_health='none'
|
|
stable_since=''
|
|
stable_restarts=''
|
|
while :; do
|
|
now="$(date +%s)"
|
|
[ $((now - started_at)) -lt "$timeout" ] || break
|
|
last_state="$(docker inspect --format '{{.State.Status}}' "$name" 2>/dev/null || true)"
|
|
last_health="$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$name" 2>/dev/null || true)"
|
|
if [ "$last_state" = 'running' ]; then
|
|
if [ "$expected" = 'healthy' ] && [ "$last_health" = 'healthy' ]; then
|
|
return 0
|
|
fi
|
|
if [ "$expected" = 'stable' ]; then
|
|
restarts="$(docker inspect --format '{{.RestartCount}}' "$name" 2>/dev/null || true)"
|
|
if [ "$restarts" != "$stable_restarts" ] || [ -z "$stable_since" ]; then
|
|
stable_restarts="$restarts"
|
|
stable_since="$now"
|
|
elif [ $((now - stable_since)) -ge 15 ]; then
|
|
return 0
|
|
fi
|
|
fi
|
|
else
|
|
stable_since=''
|
|
stable_restarts=''
|
|
fi
|
|
sleep 1
|
|
done
|
|
echo "服务 $name 未就绪(状态:${last_state:-missing},健康:${last_health:-none}),最近日志:" >&2
|
|
docker logs --tail 30 "$name" >&2 || true
|
|
return 1
|
|
}
|
|
|
|
restore_domain_files() {
|
|
cp ".env.before-domain-$stamp" .env
|
|
cp "caddy/Caddyfile.before-domain-$stamp" caddy/Caddyfile
|
|
}
|
|
|
|
restore_domain_config() {
|
|
restore_domain_files
|
|
docker compose up -d --force-recreate --no-deps $services || true
|
|
if [ "$(sed -n 's/^CLOVER_SHARED_CADDY=//p' .env | head -n 1)" = "1" ]; then
|
|
wait_for_container clover-certificate-helper stable 30 || true
|
|
docker exec clover-certificate-helper python3 /app/host-caddy.py configure-env --host-root /host --env-file /clover/.env || true
|
|
fi
|
|
wait_for_container clover-panel healthy 90 || true
|
|
wait_for_container clover-caddy stable 60 || true
|
|
wait_for_container clover-certificate-helper stable 30 || true
|
|
}
|
|
|
|
wait_for_http_panel_route() {
|
|
panel_host="$1"
|
|
http_port="$2"
|
|
elapsed=0
|
|
while [ "$elapsed" -lt 30 ]; do
|
|
if docker exec clover-certificate-helper python3 -c \
|
|
'import sys,urllib.request; req=urllib.request.Request(sys.argv[1],headers={"Host":sys.argv[2]}); resp=urllib.request.urlopen(req,timeout=4); sys.exit(0 if resp.status == 200 else 1)' \
|
|
"http://127.0.0.1:${http_port}/api/health" "$panel_host" >/dev/null 2>&1; then
|
|
return 0
|
|
fi
|
|
sleep 1
|
|
elapsed=$((elapsed + 1))
|
|
done
|
|
echo '内部 Caddy 尚未能连接面板健康接口。' >&2
|
|
return 1
|
|
}
|
|
|
|
container_env_matches() {
|
|
name="$1"
|
|
key="$2"
|
|
value="$3"
|
|
docker inspect --format '{{range .Config.Env}}{{println .}}{{end}}' "$name" 2>/dev/null \
|
|
| grep -Fxq -- "$key=$value"
|
|
}
|
|
|
|
case "$action" in
|
|
restart)
|
|
echo '正在重启面板…'
|
|
docker compose restart panel
|
|
echo '面板已重启。'
|
|
;;
|
|
update)
|
|
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
echo '正在备份面板数据库…'
|
|
docker exec clover-panel node -e 'const Database=require("better-sqlite3"); const db=new Database("/app/data/clover.db"); db.backup("/app/data/clover-before-update-"+process.argv[1]+".db").then(()=>db.close()).catch((err)=>{console.error(err);process.exit(1)})' "$stamp"
|
|
echo '正在准备完整更新工具…'
|
|
apk add --no-cache bash curl git openssl python3
|
|
export CLOVER_UPDATE_TO_LATEST=1
|
|
# Use the same source/scripts/Compose/image update path as the clover CLI.
|
|
exec bash scripts/upgrade.sh
|
|
;;
|
|
configure)
|
|
panel_host="${2:-}"
|
|
mail_host="${3:-}"
|
|
case "$panel_host" in *[!a-z0-9.-]*|'') echo '面板域名无效'; exit 1;; esac
|
|
case "$mail_host" in *[!a-z0-9.-]*|'') echo '邮件主机名无效'; exit 1;; esac
|
|
[ "$panel_host" != "$mail_host" ] || { echo '面板与邮件主机名必须不同'; exit 1; }
|
|
stamp="$(date -u +%Y%m%dT%H%M%SZ)"
|
|
cp .env ".env.before-domain-$stamp"
|
|
cp caddy/Caddyfile "caddy/Caddyfile.before-domain-$stamp"
|
|
previous_mail="$(sed -n 's/^MAIL_HOSTNAME=//p' .env | head -n 1)"
|
|
update_env PANEL_HOSTNAME "$panel_host"
|
|
update_env PANEL_FQDN "$panel_host"
|
|
update_env MAIL_HOSTNAME "$mail_host"
|
|
update_env MAIL_FQDN "$mail_host"
|
|
update_env OVERRIDE_HOSTNAME "$mail_host"
|
|
caddy_scheme="$(sed -n 's/^CLOVER_CADDY_SCHEME=//p' .env | head -n 1)"
|
|
[ -n "$caddy_scheme" ] || caddy_scheme='https'
|
|
caddy_tls="$(sed -n 's/^CLOVER_TLS_DIRECTIVE=//p' .env | head -n 1)"
|
|
acme_email="$(sed -n 's/^ACME_EMAIL=//p' .env | head -n 1)"
|
|
if [ "$previous_mail" = "$mail_host" ]; then
|
|
services='panel caddy certificate-helper'
|
|
else
|
|
services='mail panel caddy certificate-helper'
|
|
fi
|
|
cat > caddy/Caddyfile <<'CADDY'
|
|
{
|
|
email {$ACME_EMAIL}
|
|
}
|
|
|
|
{$CLOVER_CADDY_SCHEME}://{$PANEL_HOSTNAME} {
|
|
{$CLOVER_TLS_DIRECTIVE}
|
|
encode zstd gzip
|
|
handle /api/internal/roundcube/* {
|
|
respond 404
|
|
}
|
|
handle /webmail/* {
|
|
uri strip_prefix /webmail
|
|
reverse_proxy roundcube:80
|
|
}
|
|
handle /rspamd* {
|
|
reverse_proxy mail:11334
|
|
}
|
|
handle {
|
|
reverse_proxy panel:3000
|
|
}
|
|
}
|
|
CADDY
|
|
echo '正在验证 Caddy 配置…'
|
|
if ! docker run --rm -e PANEL_HOSTNAME="$panel_host" -e ACME_EMAIL="$acme_email" -e CLOVER_CADDY_SCHEME="$caddy_scheme" -e CLOVER_TLS_DIRECTIVE="$caddy_tls" -v "$project/caddy/Caddyfile:/etc/caddy/Caddyfile:ro" caddy:2 caddy validate --config /etc/caddy/Caddyfile; then
|
|
restore_domain_files
|
|
echo '配置验证失败,已恢复原配置。'
|
|
exit 1
|
|
fi
|
|
echo '正在应用域名配置…'
|
|
if ! docker compose up -d --force-recreate --no-deps $services; then
|
|
restore_domain_config
|
|
echo '应用失败,已尝试恢复原配置。'
|
|
exit 1
|
|
fi
|
|
if ! wait_for_container clover-panel healthy 90 || ! wait_for_container clover-caddy stable 60 || ! wait_for_container clover-certificate-helper stable 30; then
|
|
restore_domain_config
|
|
echo '新配置下的面板或 Caddy 未就绪,已恢复原配置。' >&2
|
|
exit 1
|
|
fi
|
|
if ! container_env_matches clover-panel PANEL_HOSTNAME "$panel_host" \
|
|
|| ! container_env_matches clover-panel MAIL_HOSTNAME "$mail_host" \
|
|
|| ! container_env_matches clover-caddy PANEL_HOSTNAME "$panel_host" \
|
|
|| ! container_env_matches clover-caddy CLOVER_CADDY_SCHEME "$caddy_scheme" \
|
|
|| ! container_env_matches docker-mailserver MAIL_HOSTNAME "$mail_host"; then
|
|
restore_domain_config
|
|
echo '容器环境仍未载入新域名配置,已恢复原配置。' >&2
|
|
exit 1
|
|
fi
|
|
if ! docker exec clover-caddy caddy validate --config /etc/caddy/Caddyfile; then
|
|
restore_domain_config
|
|
echo '运行中的 Caddy 配置校验失败,已恢复原配置。' >&2
|
|
exit 1
|
|
fi
|
|
if [ "$caddy_scheme" = 'http' ]; then
|
|
http_port="$(sed -n 's/^CLOVER_HTTP_PORT=//p' .env | head -n 1)"
|
|
[ -n "$http_port" ] || http_port='18080'
|
|
if ! wait_for_http_panel_route "$panel_host" "$http_port"; then
|
|
restore_domain_config
|
|
echo '新配置下内部 Caddy 无法访问面板,已恢复原配置。' >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
if [ "$(sed -n 's/^CLOVER_SHARED_CADDY=//p' .env | head -n 1)" = "1" ]; then
|
|
# This maintenance script runs inside the docker:cli container, which has
|
|
# neither Python nor the host filesystem. The certificate helper has
|
|
# both the host root mounted at /host and the project mounted at /clover.
|
|
if ! docker exec clover-certificate-helper python3 /app/host-caddy.py configure-env --host-root /host --env-file /clover/.env; then
|
|
restore_domain_config
|
|
echo '宿主机 Caddy 配置失败;已恢复 Clover 配置,原 Caddy 站点未被覆盖。' >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
echo "域名配置已应用。面板:https://$panel_host/;邮件主机:$mail_host"
|
|
;;
|
|
*) echo '用法:maintenance.sh restart|update|configure <面板域名> <邮件主机名>'; exit 2 ;;
|
|
esac
|