R12 复测:worker 226 期间 chv-sync 全程静默走直接路径回落,pull/ push/status 表面 rc=0,privsep/safety 一行没跑——回落只有一句 stderr 警告,无事后可数的痕。本提交让一次回落留两处痕:stderr 警告保留 “worker 不在”标记(措辞加“未走 worker,privsep 未生效”,供驱动 识别)+ 追加一行 JSON 审计到 $USER_HOME/.chv-sync-fallback-audit.log(CHV_SYNC_FALLBACK_LOG 可改 位置;事件/动作/仓/时间戳,delegated_to_worker=false、 privsep_active=false)。审计写失败只记 stderr,绝不中断同步(与 worker _audit 同款口径)。gap 文档诚实边界第 4 条同步更新。 取舍:不改 fail-closed,仍是 fail-open。理由:旧镜像(无 worker) 须仍能同步,改 fail-closed 会把所有旧镜像的同步当场 brick,且无 镜像外恢复手段;本轮项 1 已让新镜像的 worker 开机即起,回落只在 worker 真挂 / 旧镜像时发生,不再是常态路径。旧镜像退役后改为无 worker 即 fail-closed(原定方向不变,见 chv-sync.sh 头注)。 测试:ChvSyncDelegationTest 新增——回落审计一行 JSON(字段全)、 两次回落两行、审计落盘失败不中断同步;走 worker 时不留回落审计 (反向对照)。
200 lines
9.7 KiB
Bash
Executable File
200 lines
9.7 KiB
Bash
Executable File
#!/bin/bash
|
||
# chv-sync:轻量 VM 里的 Gitea 双向同步(用户工作区口径)。
|
||
#
|
||
# 主角是用户,不是 agent:用户在 Gitea 上的仓库在 VM 启动时
|
||
# clone 到 ~/workspace,用完再同步回 Gitea;VM 是用户的工作区,
|
||
# Gitea 是权威远端。同步口径沿用 Git Workspace Platform 已定
|
||
# 的规则:分叉或冲突时失败关闭,不强制覆盖、不自动 rebase、
|
||
# 不静默丢提交。
|
||
#
|
||
# 凭证绝不进镜像。启动时由 seed 盘投递两份文件(0600,root 所有):
|
||
# /etc/chv/guest-env(非密钥同步变量:GITEA_URL/GITEA_USER/
|
||
# SYNC_REPO/SYNC_BRANCH/GITEA_EMAIL)与
|
||
# /etc/chv/authd-seed(feat-chv-muse-p3:只装 GITEA_TOKEN,
|
||
# 只给客内 authd 守护进程读)。
|
||
# boot(root,cloud-init runcmd 调)读 guest-env,authd 在时配
|
||
# git credential helper 走 authd 按次取凭证(只进 git 子进程
|
||
# 内存,不落盘);authd 不在时直接失败(round-11 真机修复层 5:
|
||
# 旧镜像的落盘回落已废止——它在真机测试里把 authd 释放的长效
|
||
# token 写进了客内 ~/.git-credentials;authd helper 是唯一凭证
|
||
# helper,且自身绝不落盘)。pull/push/status 以 chv 身份跑,用
|
||
# helper,不再读 guest-env;token 不会出现在 git remote URL 或
|
||
# 镜像里。
|
||
#
|
||
# 用法:
|
||
# chv-sync boot # 启动时 clone(已 clone 则静默退出,不自动 pull)
|
||
# chv-sync pull [仓目录] # 脏树拒绝;只接受 fast-forward,分叉即失败
|
||
# chv-sync push [仓目录] # 普通 push;无 upstream 时 push -u origin HEAD;
|
||
# # 非 fast-forward 即失败,绝不 --force
|
||
# chv-sync status [仓目录] # 本地状态 + 最近提交
|
||
# 仓目录省略时取 $WORKSPACE 下唯一的 git 仓库;多个时必须显式给。
|
||
set -euo pipefail
|
||
|
||
GUEST_ENV="${CHV_GUEST_ENV:-/etc/chv/guest-env}"
|
||
WORKSPACE="${CHV_WORKSPACE:-/home/chv/workspace}"
|
||
USER_HOME="${CHV_USER_HOME:-/home/chv}"
|
||
SYNC_USER="${CHV_USER:-chv}"
|
||
# privsep worker(feat-chv-muse-p6):socket 在时 pull/push/status
|
||
# 改走 worker(类型化请求),不在时走旧的直接路径(过渡兼容)。
|
||
# round-12 修复轮第二轮(项 2):回落仍是 fail-open,不改
|
||
# fail-closed——旧镜像(无 worker)须仍能同步,改 fail-closed 会把
|
||
# 所有旧镜像的同步当场 brick,且无镜像外恢复手段;本轮项 1 已让新
|
||
# 镜像的 worker 开机即起,回落只在 worker 真挂 / 旧镜像时发生,不
|
||
# 再是常态路径。但回落不再静默:每次回落都在 stderr 警告(保留
|
||
# “worker 不在”标记供驱动识别)+ 追加一行 JSON 审计
|
||
# ($USER_HOME/.chv-sync-fallback-audit.log,可用
|
||
# CHV_SYNC_FALLBACK_LOG 改位置;写失败只记 stderr,绝不中断同步,
|
||
# 与 worker 的 _audit 同款口径)。事后 grep 该审计文件即能数出回落
|
||
# 次数、动作与仓。旧镜像退役后改为无 worker 即 fail-closed。
|
||
WORKER_SOCK="${CHV_GITEA_WORKER_SOCK:-/run/chv-gitea/worker.sock}"
|
||
WORKER_CLIENT="${CHV_GITEA_WORKER_CLIENT:-/usr/local/bin/chv-sync-worker-client}"
|
||
|
||
die() { echo "chv-sync: $*" >&2; exit 1; }
|
||
info() { echo "chv-sync: $*"; }
|
||
|
||
# 试走 worker:返回 42 = worker 不在(调用方走旧路径);其它返回
|
||
# 值即 worker 的退出码(调用方直接退出,不再回落重跑)。
|
||
try_worker() {
|
||
local action="$1"; shift
|
||
[ -S "$WORKER_SOCK" ] || return 42
|
||
[ -x "$WORKER_CLIENT" ] || return 42
|
||
"$WORKER_CLIENT" "$action" "$@"
|
||
}
|
||
|
||
# 回落显性化(round-12 修复轮第二轮,项 2):一次回落留两处痕——
|
||
# stderr 警告 + 一行 JSON 审计(事件/动作/仓/时间戳,明确记
|
||
# delegated_to_worker=false、privsep_active=false)。审计写失败
|
||
# 只记 stderr,绝不中断同步。
|
||
fallback_warn() {
|
||
local action="$1" repo="${2:-}"
|
||
echo "chv-sync: worker 不在,走旧的直接路径(过渡兼容):未走 worker,privsep 未生效" >&2
|
||
local log="${CHV_SYNC_FALLBACK_LOG:-$USER_HOME/.chv-sync-fallback-audit.log}"
|
||
local ts
|
||
ts="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo unknown)"
|
||
repo="${repo//\"/}"
|
||
printf '%s\n' "{\"event\":\"chv_sync_worker_fallback\",\"action\":\"$action\",\"repo\":\"$repo\",\"delegated_to_worker\":false,\"privsep_active\":false,\"ts\":\"$ts\"}" >> "$log" 2>/dev/null \
|
||
|| echo "chv-sync: 回落审计落盘失败:$log" >&2
|
||
}
|
||
|
||
# 解析仓目录:显式给则用它;否则取 WORKSPACE 下唯一的 git 仓库。
|
||
resolve_repo() {
|
||
if [ -n "${1:-}" ]; then
|
||
[ -d "$1/.git" ] || die "不是 git 仓库:$1"
|
||
printf '%s' "$1"
|
||
return 0
|
||
fi
|
||
local found=()
|
||
local d
|
||
for d in "$WORKSPACE"/*/; do
|
||
[ -d "${d}.git" ] && found+=("${d%/}")
|
||
done
|
||
if [ "${#found[@]}" -eq 1 ]; then
|
||
printf '%s' "${found[0]}"
|
||
return 0
|
||
fi
|
||
die "WORKSPACE 下 git 仓库不是唯一(${#found[@]} 个),请显式给仓目录"
|
||
}
|
||
|
||
cmd_boot() {
|
||
# 无 guest-env = 本次启动不同步任何仓,静默成功(VM 照常用)。
|
||
[ -f "$GUEST_ENV" ] || { info "无 $GUEST_ENV,本次启动不同步"; exit 0; }
|
||
# shellcheck disable=SC1090
|
||
set -a; . "$GUEST_ENV"; set +a
|
||
: "${GITEA_URL:?guest-env 缺 GITEA_URL}"
|
||
: "${GITEA_USER:?guest-env 缺 GITEA_USER}"
|
||
: "${SYNC_REPO:?guest-env 缺 SYNC_REPO}"
|
||
[ "$(id -u)" = "0" ] || die "boot 须以 root 运行(cloud-init runcmd)"
|
||
local name="${SYNC_REPO##*/}"
|
||
local dir="$WORKSPACE/$name"
|
||
mkdir -p "$WORKSPACE"
|
||
chown "$SYNC_USER:$SYNC_USER" "$WORKSPACE"
|
||
# feat-chv-muse-p3:authd 路径——socket 存在时凭证走 helper,
|
||
# 不再把真 token 写进 .git-credentials 落盘;helper 按次向
|
||
# authd 取凭证,只进 git 子进程内存。socket 不存在时直接失败
|
||
#(round-11 真机修复层 5:落盘回落已废止,见下)。
|
||
# 注意:authd 路径下 GITEA_TOKEN 不在 guest-env 里(它只进
|
||
# authd-seed 给守护进程),所以这里不要求它。
|
||
local authd_socket="${CHV_AUTHD_SOCKET:-/run/chv-authd/authd.sock}"
|
||
local helper="/usr/local/bin/git-credential-chv-authd"
|
||
if [ -S "$authd_socket" ] && [ -x "$helper" ]; then
|
||
runuser -u "$SYNC_USER" -- env HOME="$USER_HOME" git config --global credential.helper "$helper"
|
||
# helper 按路径推断仓库与操作(Sentinel L7 绑定),git
|
||
# 默认不把 path 发给 helper(须 credential.useHttpPath),
|
||
# 不开它每次都会因“请求不完整”拒绝——新测试抓到的真问题。
|
||
runuser -u "$SYNC_USER" -- env HOME="$USER_HOME" git config --global credential.useHttpPath true
|
||
info "凭证走 chv-authd(不落盘)"
|
||
else
|
||
# round-11 真机修复(层 5):authd 不在时直接失败,不再
|
||
# 把真 token 写进 ~/.git-credentials 落盘——旧路径在
|
||
# round-11 真机测试里把 authd 释放的长效 token 写进了
|
||
# 客内磁盘(/home/chv/.git-credentials,0600)。authd
|
||
# helper 是唯一凭证 helper,且自身绝不落盘;旧镜像退役
|
||
# 后过渡兼容即废止,不再留落盘逃生口。
|
||
die "chv-authd 未运行(socket 或 helper 缺失),拒绝同步:凭证绝不落盘,请用含 authd 的镜像"
|
||
fi
|
||
if [ -d "$dir/.git" ]; then
|
||
info "已 clone:$dir(不自动 pull,改动由用户显式同步)"
|
||
exit 0
|
||
fi
|
||
[ -e "$dir" ] && die "$dir 已存在但不是 git 仓库,请人工处理"
|
||
local clone_args=(clone)
|
||
[ -n "${SYNC_BRANCH:-}" ] && clone_args+=(--branch "$SYNC_BRANCH")
|
||
clone_args+=("$GITEA_URL/$SYNC_REPO.git" "$dir")
|
||
runuser -u "$SYNC_USER" -- env HOME="$USER_HOME" git "${clone_args[@]}"
|
||
runuser -u "$SYNC_USER" -- env HOME="$USER_HOME" git -C "$dir" config user.name "$GITEA_USER"
|
||
if [ -n "${GITEA_EMAIL:-}" ]; then
|
||
runuser -u "$SYNC_USER" -- env HOME="$USER_HOME" git -C "$dir" config user.email "$GITEA_EMAIL"
|
||
fi
|
||
info "已 clone $SYNC_REPO 到 $dir,HEAD=$(runuser -u "$SYNC_USER" -- env HOME="$USER_HOME" git -C "$dir" rev-parse --short HEAD)"
|
||
}
|
||
|
||
cmd_pull() {
|
||
local rc=0
|
||
try_worker pull "${1:-}" || rc=$?
|
||
[ "$rc" != "42" ] && exit "$rc"
|
||
fallback_warn pull "${1:-}"
|
||
local dir
|
||
dir="$(resolve_repo "${1:-}")"
|
||
[ -z "$(git -C "$dir" status --porcelain)" ] \
|
||
|| die "工作树有未提交改动,拒绝 pull(先 commit 或 stash)"
|
||
git -C "$dir" pull --ff-only \
|
||
|| die "pull 不是 fast-forward(本地与远端已分叉),请人工处理,不自动 rebase/merge"
|
||
info "已同步:$dir HEAD=$(git -C "$dir" rev-parse --short HEAD)"
|
||
}
|
||
|
||
cmd_push() {
|
||
local rc=0
|
||
try_worker push "${1:-}" || rc=$?
|
||
[ "$rc" != "42" ] && exit "$rc"
|
||
fallback_warn push "${1:-}"
|
||
local dir
|
||
dir="$(resolve_repo "${1:-}")"
|
||
if git -C "$dir" rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1; then
|
||
git -C "$dir" push \
|
||
|| die "push 被拒绝(远端已有新提交,先 pull 再 push,不 --force)"
|
||
else
|
||
git -C "$dir" push -u origin HEAD \
|
||
|| die "push 被拒绝(远端已有新提交,先 pull 再 push,不 --force)"
|
||
fi
|
||
info "已 push:$dir HEAD=$(git -C "$dir" rev-parse --short HEAD)"
|
||
}
|
||
|
||
cmd_status() {
|
||
local rc=0
|
||
try_worker status "${1:-}" || rc=$?
|
||
[ "$rc" != "42" ] && exit "$rc"
|
||
fallback_warn status "${1:-}"
|
||
local dir
|
||
dir="$(resolve_repo "${1:-}")"
|
||
git -C "$dir" status -sb
|
||
git -C "$dir" log --oneline -5
|
||
}
|
||
|
||
case "${1:-}" in
|
||
boot) shift; cmd_boot "$@" ;;
|
||
pull) shift; cmd_pull "$@" ;;
|
||
push) shift; cmd_push "$@" ;;
|
||
status) shift; cmd_status "$@" ;;
|
||
*) echo "用法:chv-sync boot|pull|push|status [仓目录]" >&2; exit 2 ;;
|
||
esac
|