Use Agent for native, container, systemd and OpenWrt installations and signed updates. Remove retired Probe deployment templates and smoke scripts.
159 lines
8.8 KiB
Bash
159 lines
8.8 KiB
Bash
#!/bin/sh
|
||
# 部署或更新容器服务(Agent;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
|
||
# 更新(配置迁移、单元和更新器、镜像):sudo sh deploy-node.sh agent (其余参数沿用上次的更新器配置)
|
||
#
|
||
# 需要 root、systemd,以及 Podman 或 Docker;脚本要和 packaging/ 里的 systemd/、update/ 放在一起。
|
||
# rootless Podman 见 docs/ops/deployment.md。
|
||
set -eu
|
||
|
||
usage() {
|
||
echo "usage: deploy-node.sh <agent> [--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) ;; *) 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 一样),注册之后面板只认这把公钥
|
||
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
|
||
|
||
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"
|