From 388619b8b6d569482dfd617ca5774b2caff94ca3 Mon Sep 17 00:00:00 2001 From: JustMe Date: Wed, 7 Oct 2026 14:17:50 +0800 Subject: [PATCH] feat: add a re-runnable deploy script for container nodes and migrate listen.bind to :: [CI SKIP] --- docs/deployment.md | 14 +++- packaging/container/deploy-node.sh | 126 +++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 packaging/container/deploy-node.sh diff --git a/docs/deployment.md b/docs/deployment.md index 0c61639..4c4489b 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -207,13 +207,25 @@ OpenWrt 默认不带 OpenSSL:`opkg install libopenssl3`(较新的快照和 2 镜像和 Compose 配置共用。普通用户执行 Podman 即为 rootless,使用命名 volume 不需要 `keep-id` 或手动 `chown`。Agent 的配置、原有身份和缓存保存在 `moshiro-agent-config`,Probe 保存在 `moshiro-probe-config`,更新状态使用独立的 `moshiro--status` volume,在业务容器内只读。不会重新生成已有身份。更新/重建容器不删除 volume;不要使用 `compose down -v`,该参数会删除数据。 +**用脚本部署和更新(rootful Podman 或 Docker,推荐)**:`packaging/container/deploy-node.sh` 可以重复运行,每次都会拉取镜像并固定到 digest、把配置迁移到当前版本需要的样子(例如旧的 `listen.bind` 为 `0.0.0.0` 的 Agent 改成 `::`,这样 IPv6 线路才能建立)、重装单元和更新器,然后重启节点;已有的身份不会动,更新策略沿用上次的。 + +```sh +# 首次部署(需要 root;脚本和 packaging/ 里的 systemd/、update/ 放在一起) +sudo sh deploy-node.sh agent --image-repo registry.oeo.one/moshiro/moshiro-agent --server https:// --join --name +sudo sh deploy-node.sh probe --image-repo registry.oeo.one/moshiro/moshiro-probe --probe-json ./probe.json +# 之后更新节点:不带参数 +sudo sh deploy-node.sh agent +``` + +节点上看不到公网 IPv6 地址,所以 Agent 的 `ipv6_host` 要在管理端手动填,Backend 才会让 Probe 去拨 IPv6。下面是不用脚本的手动步骤。 + 在 `packaging/compose/agent` 下首次注册,并写配置(下列 `podman` 可替换为 `docker`): ```sh podman compose run --rm --entrypoint /usr/local/bin/fleet-enroll agent \ --server https:// --join --name --identity-dir /etc/moshiro/agent-id podman compose run --rm -T --entrypoint /bin/sh agent -c 'cat > /etc/moshiro/agent.json' <<'JSON' -{"fleet":{"server":"https://","identity_dir":"/etc/moshiro/agent-id"},"listen":{"bind":"0.0.0.0"}} +{"fleet":{"server":"https://","identity_dir":"/etc/moshiro/agent-id"},"listen":{"bind":"::"}} JSON podman compose up -d ``` diff --git a/packaging/container/deploy-node.sh b/packaging/container/deploy-node.sh new file mode 100644 index 0000000..57f1c8e --- /dev/null +++ b/packaging/container/deploy-node.sh @@ -0,0 +1,126 @@ +#!/bin/sh +# 部署或更新容器节点(Agent、Probe)。可以重复运行:每次都会拉取镜像、把配置迁移到当前版本需要的样子、 +# 重新安装单元和更新器,然后重启节点;已有的身份和配置不会被覆盖。 +# +# 首次部署 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 --probe-json ./probe.json +# 更新(配置迁移、单元和更新器、镜像):sudo sh deploy-node.sh agent (其余参数沿用上次的更新器配置) +# +# 需要 root、systemd,以及 Podman 或 Docker;脚本要和 packaging/ 里的 systemd/、update/ 放在一起。 +# rootless Podman 见 docs/deployment.md。 +set -eu + +usage() { + echo "usage: deploy-node.sh [--engine podman|docker] [--image-repo R] [--tag T] [--image REF]" >&2 + echo " [--server URL --join TOKEN --name NAME] [--probe-json FILE] [--policy auto|manual] [--authfile F]" >&2 + exit 2 +} + +ROLE="${1:-}"; shift || true +case "$ROLE" in agent|probe) ;; *) usage ;; esac +ENGINE=""; REPO=""; TAG=latest; IMAGE=""; SERVER=""; JOIN=""; NAME=""; PROBE_JSON=""; POLICY=""; AUTHFILE="" +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 ;; + --probe-json) PROBE_JSON="$2"; shift 2 ;; + --policy) POLICY="$2"; shift 2 ;; + --authfile) AUTHFILE="$2"; shift 2 ;; + *) usage ;; + esac +done +[ "$(id -u)" -eq 0 ] || { echo "run as root (rootless Podman: see docs/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 "$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="moshiro-$ROLE-container.service" ;; + docker) UNIT="moshiro-$ROLE-docker.service" ;; + *) 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" "$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,后面的单元、辅助容器都用同一个引用 +if [ -z "$IMAGE" ]; then + if [ "$ENGINE" = docker ] && [ -n "$AUTHFILE" ]; then export DOCKER_CONFIG="$(dirname "$AUTHFILE")"; fi + if [ "$ENGINE" = podman ] && [ -n "$AUTHFILE" ]; then "$ENGINE" pull -q --authfile "$AUTHFILE" "$REPO:$TAG" >/dev/null + else "$ENGINE" pull -q "$REPO:$TAG" >/dev/null; fi + 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 +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' &2; exit 1; } + "$ENGINE" run --rm --pull=never -v "$V_CFG:/etc/moshiro" --entrypoint /usr/local/bin/fleet-enroll "$IMAGE" \ + --server "$SERVER" --join "$JOIN" --name "$NAME" --identity-dir /etc/moshiro/agent-id + fi + if ! in_volume 'test -f /etc/moshiro/agent.json' &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' ::" + fi +else + if [ -n "$PROBE_JSON" ]; then + in_volume 'umask 077; cat > /etc/moshiro/probe.json' < "$PROBE_JSON" + echo "config: imported probe.json" + elif ! in_volume 'test -f /etc/moshiro/probe.json' &2; exit 1 + fi +fi + +# 4. 单元和更新器(保持原来的更新策略) +install -d -m 755 "$CONF_DIR" +printf 'IMAGE=%s\n' "$IMAGE" > "$CONF_DIR/$ROLE.image" +install -m 644 "$PACKAGING/systemd/$UNIT" "/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"