Files
MoShiro/packaging/container/deploy-node.sh
T
justme 68c7113adf
ci/woodpecker/push/web-build Pipeline was successful
ci/woodpecker/push/verify Pipeline was successful
ci/woodpecker/push/dev-image Pipeline was successful
feat(update): update the backend like nodes via quadlet restart [CI]
2026-10-18 00:36:38 +08:00

172 lines
9.8 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/sh
# 部署或更新容器服务(Agent、Probe;Backend 用 Quadlet,见 packaging/quadlet/moshiro-backend.container)。可以重复运行:每次都会拉取镜像、把配置迁移到当前版本需要的样子、
# 重新安装单元和更新器,然后重启节点;已有的身份和配置不会被覆盖。
#
# 首次部署 Agent:sudo sh deploy-node.sh agent --image-repo REPO --server https://panel --join jt_xxx --name hk-1
# 首次部署 Probe:sudo sh deploy-node.sh probe --image-repo REPO --server https://panel --join jt_xxx --name home-1
# 更新(配置迁移、单元和更新器、镜像):sudo sh deploy-node.sh agent (其余参数沿用上次的更新器配置)
# 旧版 Probe(probe.json 里没有 fleet 段):加上 --server --join --name 重新运行即可,旧文件备份为 probe.json.old
#
# 需要 root、systemd,以及 Podman 或 Docker;脚本要和 packaging/ 里的 systemd/、update/ 放在一起。
# rootless Podman 见 docs/ops/deployment.md。
set -eu
usage() {
echo "usage: deploy-node.sh <agent|probe> [--engine podman|docker] [--image-repo R] [--channel dev|release] [--tag T] [--image REF]" >&2
echo " [--server URL --join TOKEN --name NAME] [--policy auto|manual] [--authfile F]" >&2
exit 2
}
ROLE="${1:-}"; shift || true
case "$ROLE" in agent|probe) ;; *) usage ;; esac
ENGINE=""; REPO=""; TAG=""; IMAGE=""; SERVER=""; JOIN=""; NAME=""; POLICY=""; AUTHFILE=""; CHANNEL=""
while [ $# -gt 0 ]; do
case "$1" in
--engine) ENGINE="$2"; shift 2 ;;
--image-repo) REPO="$2"; shift 2 ;;
--tag) TAG="$2"; shift 2 ;;
--image) IMAGE="$2"; shift 2 ;;
--server) SERVER="$2"; shift 2 ;;
--join) JOIN="$2"; shift 2 ;;
--name) NAME="$2"; shift 2 ;;
--policy) POLICY="$2"; shift 2 ;;
--authfile) AUTHFILE="$2"; shift 2 ;;
--channel) CHANNEL="$2"; shift 2 ;;
*) usage ;;
esac
done
[ "$(id -u)" -eq 0 ] || { echo "run as root (rootless Podman: see docs/ops/deployment.md)" >&2; exit 1; }
HERE="$(cd "$(dirname "$0")" && pwd)"
PACKAGING="$(cd "$HERE/.." && pwd)"
CONF_DIR=/etc/moshiro-update
CONF="$CONF_DIR/$ROLE.conf"
# 重复运行时沿用上次的更新器配置(仓库、策略、认证文件、引擎)
conf_get() { [ -r "$CONF" ] && sed -n "s/^$1='\\(.*\\)'\$/\\1/p" "$CONF" | head -n 1 || true; }
[ -n "$REPO" ] || REPO="$(conf_get IMAGE_REPO)"
[ -n "$POLICY" ] || POLICY="$(conf_get POLICY)"
[ -n "$AUTHFILE" ] || AUTHFILE="$(conf_get AUTHFILE)"
[ -n "$ENGINE" ] || ENGINE="$(conf_get ENGINE)"
[ -n "$CHANNEL" ] || CHANNEL="$(conf_get CHANNEL)"
[ -n "$REPO" ] || REPO="registry.oeo.one/moshiro/moshiro-$ROLE"
[ -n "$POLICY" ] || POLICY=manual
if [ -z "$ENGINE" ]; then
if command -v podman >/dev/null 2>&1; then ENGINE=podman; elif command -v docker >/dev/null 2>&1; then ENGINE=docker; else echo "podman or docker is required" >&2; exit 1; fi
fi
case "$ENGINE" in
podman) UNIT_SRC="moshiro-$ROLE-container.service"; UNIT="$UNIT_SRC" ;;
docker) UNIT_SRC="moshiro-$ROLE-docker.service"; UNIT="$UNIT_SRC" ;;
*) echo "--engine must be podman or docker" >&2; exit 2 ;;
esac
V_CFG="moshiro-$ROLE-config"
V_STATUS="moshiro-$ROLE-status"
for f in "$PACKAGING/systemd/$UNIT_SRC" "$PACKAGING/update/install-updater.sh" "$PACKAGING/update/moshiro-update.sh" "$PACKAGING/update/release-key.pub.pem"; do
[ -r "$f" ] || { echo "missing $f (keep deploy-node.sh next to the packaging/ directory)" >&2; exit 1; }
done
# 1. 镜像:固定到 digest,后面的单元、辅助容器都用同一个引用。
# 默认从仓库里的签名清单取(<仓库>-manifest:<通道>,CI 发布的),用随脚本带的发布公钥验签,按清单里的 digest 固定——
# 仓库和网络都不需要可信,也不需要面板在线。
# --tag 指定标签就不验签(拉那个标签再固定 digest);--image 直接用你给的引用。
if [ "$ENGINE" = docker ] && [ -n "$AUTHFILE" ]; then export DOCKER_CONFIG="$(dirname "$AUTHFILE")"; fi
pull_quiet() {
if [ "$ENGINE" = podman ] && [ -n "$AUTHFILE" ]; then "$ENGINE" pull -q --authfile "$AUTHFILE" "$1" >/dev/null
else "$ENGINE" pull -q "$1" >/dev/null; fi
}
resolve_verified() {
channel="$CHANNEL"
if [ -z "$channel" ]; then
case "$REPO" in */moshiro-dev/*) channel=dev ;; */moshiro/*) channel=release ;; esac
fi
[ -n "$channel" ] || { echo "cannot tell the release channel from $REPO: pass --channel dev|release, or --tag to skip verification" >&2; return 1; }
command -v openssl >/dev/null 2>&1 || { echo "openssl is required to verify the release signature (or pass --tag to skip verification)" >&2; return 1; }
ref="$REPO-manifest:$channel"
pull_quiet "$ref" || { echo "cannot pull the signed manifest $ref: is the repository readable (--authfile)? Or pass --tag to skip verification" >&2; return 1; }
labels="$("$ENGINE" image inspect --format '{{index .Config.Labels "org.moshiro.manifest"}}|{{index .Config.Labels "org.moshiro.signature"}}' "$ref")" || return 1
vtmp="$(mktemp -d)"
printf '%s' "${labels%%|*}" | base64 -d > "$vtmp/manifest" 2>/dev/null && printf '%s' "${labels#*|}" | base64 -d > "$vtmp/sig" 2>/dev/null \
|| { rm -rf "$vtmp"; echo "the manifest in $ref is not readable" >&2; return 1; }
if ! openssl dgst -sha256 -verify "$PACKAGING/update/release-key.pub.pem" -signature "$vtmp/sig" "$vtmp/manifest" >/dev/null 2>&1; then
rm -rf "$vtmp"; echo "the signature of the manifest in $ref does not match the bundled release key; refusing to install" >&2; return 1
fi
mfield() { sed -n 's/.*"'"$1"'":"\([^"]*\)".*/\1/p' "$vtmp/manifest" | head -n 1; }
vdigest="$(mfield digest)"; vcomponent="$(mfield component)"; vimage="$(mfield image)"; vtag="$(mfield tag)"
rm -rf "$vtmp"
[ "$vcomponent" = "$ROLE" ] && [ "$vimage" = "$REPO" ] || { echo "the manifest is for $vcomponent / $vimage, not $ROLE / $REPO; refusing to install" >&2; return 1; }
case "$vdigest" in sha256:*) ;; *) echo "the manifest has no usable digest" >&2; return 1 ;; esac
[ "${#vdigest}" -eq 71 ] && [ -z "$(printf '%s' "${vdigest#sha256:}" | tr -d '0-9a-f')" ] || { echo "the manifest digest is malformed" >&2; return 1; }
IMAGE="$REPO@$vdigest"
pull_quiet "$IMAGE" || { echo "cannot pull $IMAGE" >&2; return 1; }
echo "verified release $vtag (CI-signed manifest, $ref)"
}
if [ -z "$IMAGE" ]; then
if [ -z "$TAG" ]; then
resolve_verified || exit 1
else
pull_quiet "$REPO:$TAG"
if [ "$ENGINE" = podman ]; then DIGEST="$("$ENGINE" image inspect --format '{{.Digest}}' "$REPO:$TAG")"; IMAGE="$REPO@$DIGEST"
else IMAGE="$("$ENGINE" image inspect --format '{{index .RepoDigests 0}}' "$REPO:$TAG")"; fi
echo "warning: $REPO:$TAG was not signature-checked (--tag)" >&2
fi
fi
echo "image: $IMAGE"
# 2. 卷。辅助容器以镜像自带的 app 用户(1654)读写,所以卷里的文件属主总是对的
"$ENGINE" volume inspect "$V_CFG" >/dev/null 2>&1 || "$ENGINE" volume create "$V_CFG" >/dev/null
"$ENGINE" volume inspect "$V_STATUS" >/dev/null 2>&1 || "$ENGINE" volume create "$V_STATUS" >/dev/null
in_volume() {
"$ENGINE" run --rm -i --pull=never --network none --cap-drop ALL --security-opt no-new-privileges \
-v "$V_CFG:/etc/moshiro" --entrypoint /bin/sh "$IMAGE" -c "$1"
}
in_volume 'mkdir -p /etc/moshiro/update' </dev/null
# 3. 配置:身份由节点自己生成(Agent 和 Probe 一样),注册之后面板只认这把公钥
ID_DIR="/etc/moshiro/$ROLE-id"
if ! in_volume "test -f $ID_DIR/node.json" </dev/null; then
[ -n "$SERVER" ] && [ -n "$JOIN" ] && [ -n "$NAME" ] || { echo "first deployment needs --server, --join and --name" >&2; exit 1; }
"$ENGINE" run --rm --pull=never --network host -v "$V_CFG:/etc/moshiro" --entrypoint /usr/local/bin/fleet-enroll "$IMAGE" \
--server "$SERVER" --join "$JOIN" --name "$NAME" --identity-dir "$ID_DIR"
fi
if [ "$ROLE" = agent ]; then
if ! in_volume 'test -f /etc/moshiro/agent.json' </dev/null; then
[ -n "$SERVER" ] || { echo "agent.json is missing; pass --server to create it" >&2; exit 1; }
printf '{"fleet":{"server":"%s","identity_dir":"/etc/moshiro/agent-id"},"listen":{"bind":"::"}}\n' "$SERVER" \
| in_volume 'cat > /etc/moshiro/agent.json'
echo "config: created agent.json (listen.bind ::)"
elif in_volume 'grep -q "\"bind\" *: *\"0\.0\.0\.0\"" /etc/moshiro/agent.json' </dev/null; then
# 旧配置只监听 IPv4:改成 ::(双栈,IPv4 照常可用)
in_volume 'sed -i "s/\"bind\" *: *\"0\.0\.0\.0\"/\"bind\": \"::\"/" /etc/moshiro/agent.json' </dev/null
echo "config: listen.bind 0.0.0.0 -> ::"
fi
elif [ "$ROLE" = probe ]; then
# 旧格式(面板生成的 probe.json:没有 fleet 段)备份之后按新格式重写
if in_volume 'test -f /etc/moshiro/probe.json' </dev/null && ! in_volume 'grep -q "\"fleet\"" /etc/moshiro/probe.json' </dev/null; then
in_volume 'mv /etc/moshiro/probe.json /etc/moshiro/probe.json.old' </dev/null
echo "config: old probe.json moved to probe.json.old"
fi
if ! in_volume 'test -f /etc/moshiro/probe.json' </dev/null; then
[ -n "$SERVER" ] || { echo "probe.json is missing; pass --server to create it" >&2; exit 1; }
printf '{"fleet":{"server":"%s","identity_dir":"/etc/moshiro/probe-id"}}\n' "$SERVER" \
| in_volume 'umask 077; cat > /etc/moshiro/probe.json'
echo "config: created probe.json"
fi
fi
# 4. 单元和更新器(保持原来的更新策略)
install -d -m 755 "$CONF_DIR"
printf 'IMAGE=%s\n' "$IMAGE" > "$CONF_DIR/$ROLE.image"
install -m 644 "$PACKAGING/systemd/$UNIT_SRC" "/etc/systemd/system/$UNIT"
set -- "$ROLE" --engine "$ENGINE" --volume "$V_CFG" --status-volume "$V_STATUS" --helper-image "$IMAGE" \
--image-repo "$REPO" --unit "$UNIT" --policy "$POLICY" --no-enable
[ -z "$AUTHFILE" ] || set -- "$@" --authfile "$AUTHFILE"
sh "$PACKAGING/update/install-updater.sh" "$@"
# 5. 启动
systemctl daemon-reload
systemctl enable "$UNIT" "moshiro-update-$ROLE.timer" >/dev/null 2>&1
systemctl restart "$UNIT"
systemctl start "moshiro-update-$ROLE.timer"
sleep 5
systemctl is-active "$UNIT"