#!/bin/sh # 部署或更新容器服务(Backend、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 --server https://panel --join jt_xxx --name home-1 # 首次部署 Backend:sudo sh deploy-node.sh backend --channel dev --admin-password '…' [--public-url https://panel] [--port 29181] # 更新(配置迁移、单元和更新器、镜像):sudo sh deploy-node.sh agent (其余参数沿用上次的更新器配置) # 旧版 Probe(probe.json 里没有 fleet 段):加上 --server --join --name 重新运行即可,旧文件备份为 probe.json.old # # Backend 只支持 Podman,更新策略固定为 manual(管理端确认),监听宿主网络的 --port。 # 需要 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] [--policy auto|manual] [--authfile F]" >&2 echo " backend: [--channel dev|release] [--port N] [--admin-user U] [--admin-password P] [--public-url URL]" >&2 exit 2 } ROLE="${1:-}"; shift || true case "$ROLE" in agent|probe|backend) ;; *) usage ;; esac ENGINE=""; REPO=""; TAG=latest; IMAGE=""; SERVER=""; JOIN=""; NAME=""; POLICY=""; AUTHFILE=""; CHANNEL=""; PORT=""; ADMIN_USER=""; ADMIN_PASSWORD=""; PUBLIC_URL="" 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 ;; --port) PORT="$2"; shift 2 ;; --admin-user) ADMIN_USER="$2"; shift 2 ;; --admin-password) ADMIN_PASSWORD="$2"; shift 2 ;; --public-url) PUBLIC_URL="$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 "$CHANNEL" ] || CHANNEL="$(conf_get CHANNEL)" if [ "$ROLE" = backend ]; then [ -n "$CHANNEL" ] || { echo "backend needs --channel dev or release" >&2; exit 2; } case "$CHANNEL" in dev) NAMESPACE=moshiro-dev ;; release) NAMESPACE=moshiro ;; *) echo "--channel must be dev or release" >&2; exit 2 ;; esac [ -n "$REPO" ] || REPO="registry.oeo.one/$NAMESPACE/moshiro-backend" [ -n "$PORT" ] || PORT=29181 ENGINE=podman; POLICY=manual fi [ -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" if [ "$ROLE" = backend ]; then UNIT=moshiro-backend.service; V_CFG=moshiro-backend-data; fi 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,后面的单元、辅助容器都用同一个引用 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" } [ "$ROLE" = backend ] || in_volume 'mkdir -p /etc/moshiro/update' /etc/moshiro/backend.env ) echo "config: created /etc/moshiro/backend.env" fi else ID_DIR="/etc/moshiro/$ROLE-id" if ! in_volume "test -f $ID_DIR/node.json" &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 fi if [ "$ROLE" = agent ]; then 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 elif [ "$ROLE" = probe ]; then # 旧格式(面板生成的 probe.json:没有 fleet 段)备份之后按新格式重写 if in_volume 'test -f /etc/moshiro/probe.json' &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" [ "$ROLE" != backend ] || sed -i "s/:29181/:$PORT/" "/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 [ "$ROLE" != backend ] || set -- "$@" --channel "$CHANNEL" --health-url "http://127.0.0.1:$PORT/healthz" [ -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" if [ "$ROLE" = backend ]; then n=0 until curl -fsS -o /dev/null "http://127.0.0.1:$PORT/healthz" 2>/dev/null; do n=$((n + 1)); [ "$n" -lt 30 ] || { echo "backend did not become healthy" >&2; systemctl status "$UNIT" --no-pager >&2 || true; exit 1; } sleep 2 done else sleep 5 fi systemctl is-active "$UNIT"