feat: share named volumes across Docker and rootless Podman updates [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful
ci/woodpecker/push/main-amd64-build Pipeline was successful
This commit is contained in:
16 files changed
+473
-156
No files matched your search
@@ -1,17 +1,6 @@
|
||||
# Agent:配置和身份都放在同目录的 .data/(挂载到容器的 /etc/monitor)。
|
||||
#
|
||||
# 第一次使用:
|
||||
# mkdir -p .data && sudo chown 1654:1654 .data # 容器以 UID/GID 1654 运行
|
||||
# MONITOR_JOIN_TOKEN=<加入令牌> MONITOR_SERVER=https://<backend> MONITOR_AGENT_NAME=<名字> \
|
||||
# docker compose run --rm --entrypoint /usr/local/bin/fleet-enroll agent \
|
||||
# --server "$MONITOR_SERVER" --join "$MONITOR_JOIN_TOKEN" --name "$MONITOR_AGENT_NAME" --identity-dir /etc/monitor/agent-id
|
||||
# 再在 .data/agent.json 里写(fleet.identity_dir 要和上面一致):
|
||||
# {"fleet":{"server":"https://<backend>","identity_dir":"/etc/monitor/agent-id"},"listen":{"bind":"0.0.0.0"},"limits":{"max_total_mbps":100,"max_total_pps":10000}}
|
||||
# sudo chown -R 1654:1654 .data
|
||||
# sudo podman compose up -d # Linux rootful Podman;也可用 docker compose
|
||||
#
|
||||
# 更新:改 MONITOR_VERSION(或 MONITOR_IMAGE),再 docker compose pull && docker compose up -d。
|
||||
# 这种方式没有宿主机更新器:管理端的「更新」按钮对它不起作用;要自动更新请用 podman + systemd(docs/deployment.md「容器节点的运行」)。.data 不动,身份和历史都保留。
|
||||
# Docker 和 rootless/rootful Podman 共用。配置、身份和缓存保存在命名 volume 中。
|
||||
# docker compose up -d / podman compose up -d(Podman 以普通用户执行即可 rootless)。
|
||||
# 首次注册、配置导入和自动更新见 docs/deployment.md。
|
||||
services:
|
||||
agent:
|
||||
image: ${MONITOR_IMAGE:-registry.oeo.one/monitor/monitor-agent}:${MONITOR_VERSION:-0.1.1}
|
||||
@@ -21,7 +10,8 @@ services:
|
||||
# Probe 要直连 Agent 的 7443(TCP+UDP)和 7444(TCP),用宿主网络,不做端口映射
|
||||
network_mode: host
|
||||
volumes:
|
||||
- ./.data:/etc/monitor
|
||||
- agent-config:/etc/monitor
|
||||
- agent-status:/etc/monitor/update-status:ro
|
||||
- /etc/os-release:/etc/os-release:ro
|
||||
# 系统目录只读,不加 :z/:Z;SELinux 隔离在容器级别禁用。
|
||||
- /proc:/host/proc:ro
|
||||
@@ -43,3 +33,9 @@ services:
|
||||
# cap_add:
|
||||
# - NET_RAW
|
||||
stop_grace_period: 20s
|
||||
|
||||
volumes:
|
||||
agent-config:
|
||||
name: monitor-agent-config
|
||||
agent-status:
|
||||
name: monitor-agent-status
|
||||
@@ -0,0 +1,15 @@
|
||||
# Docker / Podman 共用,数据库和原始数据保存在命名 volume 中。
|
||||
services:
|
||||
backend:
|
||||
image: ${MONITOR_IMAGE:-registry.oeo.one/monitor/monitor-backend}:${MONITOR_VERSION:-0.1.1}
|
||||
container_name: monitor-backend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:8080:8080"
|
||||
environment:
|
||||
Monitor__InitialAdminPassword: ${MONITOR_INITIAL_ADMIN_PASSWORD:?Set MONITOR_INITIAL_ADMIN_PASSWORD}
|
||||
volumes:
|
||||
- backend-data:/data
|
||||
volumes:
|
||||
backend-data:
|
||||
name: monitor-backend-data
|
||||
@@ -1,13 +1,6 @@
|
||||
# Probe:身份配置 probe.json 和 Agent 列表缓存 agents.json 都放在同目录的 .data/(挂载到容器的 /etc/monitor)。
|
||||
#
|
||||
# 第一次使用:
|
||||
# mkdir -p .data && sudo chown 1654:1654 .data # 容器以 UID/GID 1654 运行,需要读写这个目录
|
||||
# 在管理端创建 Probe,下载一次性身份配置,保存为 .data/probe.json(含私钥,权限 600,不要提交到版本控制)
|
||||
# sudo chown -R 1654:1654 .data # probe.json 的 600 权限要求文件也归容器用户所有
|
||||
# sudo podman compose up -d # Linux rootful Podman;也可用 docker compose
|
||||
#
|
||||
# 更新:改 MONITOR_VERSION(或 MONITOR_IMAGE),再 docker compose pull && docker compose up -d。
|
||||
# 这种方式没有宿主机更新器:管理端的「更新」按钮对它不起作用;要自动更新请用 podman + systemd(docs/deployment.md「容器节点的运行」)。.data 不动。
|
||||
# Docker 和 rootless/rootful Podman 共用。配置、身份和缓存保存在命名 volume 中。
|
||||
# docker compose up -d / podman compose up -d(Podman 以普通用户执行即可 rootless)。
|
||||
# 首次注册、配置导入和自动更新见 docs/deployment.md。
|
||||
services:
|
||||
probe:
|
||||
image: ${MONITOR_IMAGE:-registry.oeo.one/monitor/monitor-probe}:${MONITOR_VERSION:-0.1.1}
|
||||
@@ -16,7 +9,8 @@ services:
|
||||
init: true
|
||||
network_mode: host
|
||||
volumes:
|
||||
- ./.data:/etc/monitor
|
||||
- probe-config:/etc/monitor
|
||||
- probe-status:/etc/monitor/update-status:ro
|
||||
- /etc/os-release:/etc/os-release:ro
|
||||
read_only: true
|
||||
tmpfs:
|
||||
@@ -30,3 +24,9 @@ services:
|
||||
# cap_add:
|
||||
# - NET_RAW
|
||||
stop_grace_period: 20s
|
||||
|
||||
volumes:
|
||||
probe-config:
|
||||
name: monitor-probe-config
|
||||
probe-status:
|
||||
name: monitor-probe-status
|
||||
@@ -9,7 +9,7 @@ Restart=always
|
||||
RestartSec=5
|
||||
TimeoutStopSec=30
|
||||
ExecStartPre=-/usr/bin/podman rm -f monitor-agent
|
||||
ExecStart=/usr/bin/podman run --rm --name monitor-agent --network host --read-only --security-opt no-new-privileges --security-opt label=disable --cap-drop all --tmpfs /tmp:rw,noexec,nosuid,size=64m,mode=1777 -v /etc/os-release:/etc/os-release:ro -v /proc:/host/proc:ro -v /sys:/host/sys:ro -v /:/host/root:ro -e HOST_PROC=/host/proc -e HOST_SYS=/host/sys -e HOST_ROOT=/host/root -v /opt/monitor/etc:/etc/monitor ${IMAGE}
|
||||
ExecStart=/usr/bin/podman run --rm --name monitor-agent --network host --read-only --security-opt no-new-privileges --security-opt label=disable --cap-drop all --tmpfs /tmp:rw,noexec,nosuid,size=64m,mode=1777 -v /etc/os-release:/etc/os-release:ro -v /proc:/host/proc:ro -v /sys:/host/sys:ro -v /:/host/root:ro -e HOST_PROC=/host/proc -e HOST_SYS=/host/sys -e HOST_ROOT=/host/root -v monitor-agent-config:/etc/monitor -v monitor-agent-status:/etc/monitor/update-status:ro ${IMAGE}
|
||||
ExecStop=/usr/bin/podman stop -t 20 monitor-agent
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=Monitor Agent (Docker container)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=/etc/monitor-update/agent.image
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
TimeoutStopSec=30
|
||||
ExecStartPre=-/usr/bin/docker rm -f monitor-agent
|
||||
ExecStart=/usr/bin/docker run --rm --name monitor-agent --network host --read-only --security-opt no-new-privileges --security-opt label=disable --cap-drop all --tmpfs /tmp:rw,noexec,nosuid,size=64m,mode=1777 -v /etc/os-release:/etc/os-release:ro -v /proc:/host/proc:ro -v /sys:/host/sys:ro -v /:/host/root:ro -e HOST_PROC=/host/proc -e HOST_SYS=/host/sys -e HOST_ROOT=/host/root -v monitor-agent-config:/etc/monitor -v monitor-agent-status:/etc/monitor/update-status:ro ${IMAGE}
|
||||
ExecStop=/usr/bin/docker stop -t 20 monitor-agent
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=Monitor Agent (rootless Podman container)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=%h/.config/monitor/agent.image
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
TimeoutStopSec=30
|
||||
ExecStartPre=-/usr/bin/podman rm -f monitor-agent
|
||||
ExecStart=/usr/bin/podman run --rm --name monitor-agent --network host --read-only --security-opt no-new-privileges --security-opt label=disable --cap-drop all --tmpfs /tmp:rw,noexec,nosuid,size=64m,mode=1777 -v /etc/os-release:/etc/os-release:ro -v /proc:/host/proc:ro -v /sys:/host/sys:ro -v /:/host/root:ro -e HOST_PROC=/host/proc -e HOST_SYS=/host/sys -e HOST_ROOT=/host/root -v monitor-agent-config:/etc/monitor -v monitor-agent-status:/etc/monitor/update-status:ro ${IMAGE}
|
||||
ExecStop=/usr/bin/podman stop -t 20 monitor-agent
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -9,7 +9,7 @@ Restart=always
|
||||
RestartSec=5
|
||||
TimeoutStopSec=30
|
||||
ExecStartPre=-/usr/bin/podman rm -f monitor-probe
|
||||
ExecStart=/usr/bin/podman run --rm --name monitor-probe --network host --read-only --security-opt no-new-privileges --security-opt label=disable --cap-drop all --tmpfs /tmp:rw,noexec,nosuid,size=64m,mode=1777 -v /etc/os-release:/etc/os-release:ro -v /opt/monitor-probe/etc:/etc/monitor ${IMAGE}
|
||||
ExecStart=/usr/bin/podman run --rm --name monitor-probe --network host --read-only --security-opt no-new-privileges --security-opt label=disable --cap-drop all --tmpfs /tmp:rw,noexec,nosuid,size=64m,mode=1777 -v /etc/os-release:/etc/os-release:ro -v monitor-probe-config:/etc/monitor -v monitor-probe-status:/etc/monitor/update-status:ro ${IMAGE}
|
||||
ExecStop=/usr/bin/podman stop -t 20 monitor-probe
|
||||
|
||||
[Install]
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=Monitor Probe (Docker container)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=/etc/monitor-update/probe.image
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
TimeoutStopSec=30
|
||||
ExecStartPre=-/usr/bin/docker rm -f monitor-probe
|
||||
ExecStart=/usr/bin/docker run --rm --name monitor-probe --network host --read-only --security-opt no-new-privileges --security-opt label=disable --cap-drop all --tmpfs /tmp:rw,noexec,nosuid,size=64m,mode=1777 -v /etc/os-release:/etc/os-release:ro -v monitor-probe-config:/etc/monitor -v monitor-probe-status:/etc/monitor/update-status:ro ${IMAGE}
|
||||
ExecStop=/usr/bin/docker stop -t 20 monitor-probe
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=Monitor Probe (rootless Podman container)
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=%h/.config/monitor/probe.image
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
TimeoutStopSec=30
|
||||
ExecStartPre=-/usr/bin/podman rm -f monitor-probe
|
||||
ExecStart=/usr/bin/podman run --rm --name monitor-probe --network host --read-only --security-opt no-new-privileges --security-opt label=disable --cap-drop all --tmpfs /tmp:rw,noexec,nosuid,size=64m,mode=1777 -v /etc/os-release:/etc/os-release:ro -v monitor-probe-config:/etc/monitor -v monitor-probe-status:/etc/monitor/update-status:ro ${IMAGE}
|
||||
ExecStop=/usr/bin/podman stop -t 20 monitor-probe
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -1,89 +1,155 @@
|
||||
#!/bin/sh
|
||||
# 在容器节点的宿主机上安装更新器:
|
||||
# install-updater.sh <agent|probe> --data-dir <挂载到容器 /etc/monitor 的宿主机目录> --image-repo <镜像仓库,不带 tag> --unit <容器的 systemd 单元名>
|
||||
# [--policy auto|manual] [--authfile <拉镜像用的 Registry 认证文件>]
|
||||
# 需要 root、systemd、podman、openssl。做的事:装脚本和发布公钥,写本机配置,装两个 systemd 单元(有新清单就检查,有「更新」请求就更新)。
|
||||
# 容器的单元要从 EnvironmentFile=/etc/monitor-update/<组件>.image 读 IMAGE=…,并用 ${IMAGE} 启动容器(没有这个文件时先写一份当前镜像)。
|
||||
# Install signed OCI updates for system services (Podman/Docker) or rootless Podman user services.
|
||||
# Rootless: install-updater.sh agent --user --volume V --helper-image I --image-repo R --unit U
|
||||
# System: sudo install-updater.sh agent --engine podman|docker --volume V --helper-image I --image-repo R --unit U
|
||||
set -eu
|
||||
|
||||
COMPONENT="${1:-}"; shift || true
|
||||
case "$COMPONENT" in agent|probe) ;; *) echo "usage: install-updater.sh <agent|probe> --data-dir D --image-repo R --unit U [--policy auto|manual] [--authfile F]" >&2; exit 2 ;; esac
|
||||
DATA_DIR=""; IMAGE_REPO=""; UNIT=""; POLICY="manual"; AUTHFILE=""
|
||||
case "$COMPONENT" in agent|probe) ;; *) echo "usage: install-updater.sh <agent|probe> [--user] [--engine podman|docker] --data-dir D --image-repo R --unit U [--policy auto|manual] [--authfile F] [--no-enable]" >&2; exit 2 ;; esac
|
||||
DATA_DIR=""; IMAGE_REPO=""; UNIT=""; POLICY=manual; AUTHFILE=""; ENGINE=podman; SERVICE_SCOPE=system; ENABLE=true; DATA_VOLUME=""; STATUS_VOLUME=""; HELPER_IMAGE=""
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--user) SERVICE_SCOPE=user; shift ;;
|
||||
--engine) ENGINE="$2"; shift 2 ;;
|
||||
--volume) DATA_VOLUME="$2"; shift 2 ;;
|
||||
--status-volume) STATUS_VOLUME="$2"; shift 2 ;;
|
||||
--helper-image) HELPER_IMAGE="$2"; shift 2 ;;
|
||||
--data-dir) DATA_DIR="$2"; shift 2 ;;
|
||||
--image-repo) IMAGE_REPO="$2"; shift 2 ;;
|
||||
--unit) UNIT="$2"; shift 2 ;;
|
||||
--policy) POLICY="$2"; shift 2 ;;
|
||||
--authfile) AUTHFILE="$2"; shift 2 ;;
|
||||
--no-enable) ENABLE=false; shift ;;
|
||||
*) echo "unknown option: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$DATA_DIR" ] && [ -n "$IMAGE_REPO" ] && [ -n "$UNIT" ] || { echo "--data-dir, --image-repo and --unit are required" >&2; exit 2; }
|
||||
if [ -z "$IMAGE_REPO" ] || [ -z "$UNIT" ] || { [ -z "$DATA_DIR" ] && [ -z "$DATA_VOLUME" ]; }; then
|
||||
echo "--image-repo, --unit and --volume (or legacy --data-dir) are required" >&2; exit 2
|
||||
fi
|
||||
case "$POLICY" in auto|manual) ;; *) echo "--policy must be auto or manual" >&2; exit 2 ;; esac
|
||||
[ "$(id -u)" -eq 0 ] || { echo "run as root" >&2; exit 1; }
|
||||
case "$ENGINE" in podman|docker) ;; *) echo "--engine must be podman or docker" >&2; exit 2 ;; esac
|
||||
if [ -n "$DATA_VOLUME" ]; then
|
||||
[ -z "$DATA_DIR" ] || { echo "use --volume or --data-dir, not both" >&2; exit 2; }
|
||||
[ -n "$HELPER_IMAGE" ] || { echo "--volume needs --helper-image (an already pulled Monitor image)" >&2; exit 2; }
|
||||
STATUS_VOLUME="${STATUS_VOLUME:-monitor-$COMPONENT-status}"
|
||||
"$ENGINE" image inspect "$HELPER_IMAGE" >/dev/null
|
||||
"$ENGINE" volume inspect "$DATA_VOLUME" >/dev/null 2>&1 || "$ENGINE" volume create "$DATA_VOLUME" >/dev/null
|
||||
"$ENGINE" volume inspect "$STATUS_VOLUME" >/dev/null 2>&1 || "$ENGINE" volume create "$STATUS_VOLUME" >/dev/null
|
||||
else
|
||||
case "$DATA_DIR" in /*) ;; *) echo "--data-dir must be absolute" >&2; exit 2 ;; esac
|
||||
fi
|
||||
|
||||
if [ "$SERVICE_SCOPE" = user ]; then
|
||||
[ "$(id -u)" -ne 0 ] || { echo "--user must run as the rootless Podman user, without sudo" >&2; exit 1; }
|
||||
[ "$ENGINE" = podman ] || { echo "--user currently supports rootless Podman; Docker uses system services" >&2; exit 2; }
|
||||
CONF_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/monitor"
|
||||
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/monitor-update"
|
||||
UNIT_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user"
|
||||
BIN_DIR="$HOME/.local/bin"
|
||||
TARGET=default.target
|
||||
else
|
||||
[ "$(id -u)" -eq 0 ] || { echo "system installation needs root; use --user for rootless Podman" >&2; exit 1; }
|
||||
CONF_DIR=/etc/monitor-update
|
||||
STATE_DIR=/var/lib/monitor-update
|
||||
UNIT_DIR=/etc/systemd/system
|
||||
BIN_DIR=/usr/local/sbin
|
||||
TARGET=multi-user.target
|
||||
fi
|
||||
service_ctl() {
|
||||
if [ "$SERVICE_SCOPE" = user ]; then systemctl --user "$@"; else systemctl "$@"; fi
|
||||
}
|
||||
# Config is sourced by the updater; quote arguments as shell data.
|
||||
shell_quote() { printf "'"; printf '%s' "$1" | sed "s/'/'\"'\"'/g"; printf "'"; }
|
||||
unit_escape() { printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'; }
|
||||
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
install -d -m 755 /etc/monitor-update /var/lib/monitor-update
|
||||
install -m 755 "$HERE/monitor-update.sh" /usr/local/sbin/monitor-update
|
||||
install -m 644 "$HERE/release-key.pub.pem" /etc/monitor-update/release-key.pub.pem
|
||||
install -d -m 755 "$CONF_DIR" "$STATE_DIR" "$UNIT_DIR" "$BIN_DIR"
|
||||
BIN="$BIN_DIR/monitor-update"
|
||||
install -m 755 "$HERE/monitor-update.sh" "$BIN"
|
||||
install -m 644 "$HERE/release-key.pub.pem" "$CONF_DIR/release-key.pub.pem"
|
||||
if [ -n "$DATA_VOLUME" ]; then
|
||||
: # Named volumes are initialized by the image; no host directory or chown needed.
|
||||
elif [ "$SERVICE_SCOPE" = user ]; then
|
||||
install -d -m 700 "$DATA_DIR/update"
|
||||
else
|
||||
install -d -m 755 -o 1654 -g 1654 "$DATA_DIR/update"
|
||||
fi
|
||||
if [ -z "$DATA_VOLUME" ]; then install -d -m 755 "$DATA_DIR/update-status"; fi
|
||||
# Rootless service templates mount update-status separately read-only.
|
||||
ENV_FILE="$CONF_DIR/$COMPONENT.image"
|
||||
PUBKEY="$CONF_DIR/release-key.pub.pem"
|
||||
{
|
||||
for name in DATA_DIR IMAGE_REPO UNIT PUBKEY ENV_FILE STATE_DIR POLICY AUTHFILE ENGINE SERVICE_SCOPE DATA_VOLUME STATUS_VOLUME HELPER_IMAGE; do
|
||||
case "$name" in
|
||||
DATA_DIR) value="$DATA_DIR" ;; IMAGE_REPO) value="$IMAGE_REPO" ;; UNIT) value="$UNIT" ;;
|
||||
PUBKEY) value="$PUBKEY" ;; ENV_FILE) value="$ENV_FILE" ;; STATE_DIR) value="$STATE_DIR" ;;
|
||||
DATA_VOLUME) value="$DATA_VOLUME" ;; STATUS_VOLUME) value="$STATUS_VOLUME" ;; HELPER_IMAGE) value="$HELPER_IMAGE" ;;
|
||||
POLICY) value="$POLICY" ;; AUTHFILE) value="$AUTHFILE" ;; ENGINE) value="$ENGINE" ;; SERVICE_SCOPE) value="$SERVICE_SCOPE" ;;
|
||||
esac
|
||||
printf '%s=' "$name"; shell_quote "$value"; printf '\n'
|
||||
done
|
||||
} > "$CONF_DIR/$COMPONENT.conf"
|
||||
chmod 600 "$CONF_DIR/$COMPONENT.conf"
|
||||
|
||||
# 容器用户(UID 1654)能写 update/(放清单和请求);update-status/ 归 root,业务进程只读
|
||||
install -d -m 755 -o 1654 -g 1654 "$DATA_DIR/update"
|
||||
install -d -m 755 "$DATA_DIR/update-status"
|
||||
|
||||
cat > "/etc/monitor-update/$COMPONENT.conf" <<CONF
|
||||
DATA_DIR=$DATA_DIR
|
||||
IMAGE_REPO=$IMAGE_REPO
|
||||
UNIT=$UNIT
|
||||
PUBKEY=/etc/monitor-update/release-key.pub.pem
|
||||
ENV_FILE=/etc/monitor-update/$COMPONENT.image
|
||||
STATE_DIR=/var/lib/monitor-update
|
||||
POLICY=$POLICY
|
||||
AUTHFILE=$AUTHFILE
|
||||
CONF
|
||||
chmod 644 "/etc/monitor-update/$COMPONENT.conf"
|
||||
|
||||
cat > "/etc/systemd/system/monitor-update-check-$COMPONENT.service" <<UNITFILE
|
||||
if [ -n "$DATA_VOLUME" ]; then
|
||||
cat > "$UNIT_DIR/monitor-update-$COMPONENT.service" <<UNITFILE
|
||||
[Unit]
|
||||
Description=Check the signed update offer for monitor-$COMPONENT
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/monitor-update check $COMPONENT
|
||||
UNITFILE
|
||||
cat > "/etc/systemd/system/monitor-update-apply-$COMPONENT.service" <<UNITFILE
|
||||
[Unit]
|
||||
Description=Apply the verified update for monitor-$COMPONENT
|
||||
Description=Check/apply signed volume update for monitor-$COMPONENT
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
TimeoutStartSec=300
|
||||
ExecStart=/usr/local/sbin/monitor-update apply $COMPONENT
|
||||
Environment="MONITOR_UPDATE_CONF_DIR=$(unit_escape "$CONF_DIR")"
|
||||
ExecStart="$(unit_escape "$BIN")" check $COMPONENT
|
||||
UNITFILE
|
||||
cat > "/etc/systemd/system/monitor-update-check-$COMPONENT.path" <<UNITFILE
|
||||
cat > "$UNIT_DIR/monitor-update-$COMPONENT.timer" <<UNITFILE
|
||||
[Unit]
|
||||
Description=Watch for a new update offer for monitor-$COMPONENT
|
||||
Description=Poll the update volume for monitor-$COMPONENT
|
||||
|
||||
[Path]
|
||||
PathChanged=$DATA_DIR/update/offer.sig
|
||||
Unit=monitor-update-check-$COMPONENT.service
|
||||
[Timer]
|
||||
OnBootSec=10s
|
||||
OnUnitInactiveSec=30s
|
||||
Unit=monitor-update-$COMPONENT.service
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
WantedBy=$TARGET
|
||||
UNITFILE
|
||||
cat > "/etc/systemd/system/monitor-update-apply-$COMPONENT.path" <<UNITFILE
|
||||
else
|
||||
for action in check apply; do
|
||||
cat > "$UNIT_DIR/monitor-update-$action-$COMPONENT.service" <<UNITFILE
|
||||
[Unit]
|
||||
Description=Watch for an update request for monitor-$COMPONENT
|
||||
Description=$action the signed update for monitor-$COMPONENT
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
TimeoutStartSec=300
|
||||
Environment="MONITOR_UPDATE_CONF_DIR=$(unit_escape "$CONF_DIR")"
|
||||
ExecStart="$(unit_escape "$BIN")" $action $COMPONENT
|
||||
UNITFILE
|
||||
watched=offer.sig
|
||||
[ "$action" != apply ] || watched=apply.request
|
||||
cat > "$UNIT_DIR/monitor-update-$action-$COMPONENT.path" <<UNITFILE
|
||||
[Unit]
|
||||
Description=Watch $action events for monitor-$COMPONENT
|
||||
|
||||
[Path]
|
||||
PathChanged=$DATA_DIR/update/apply.request
|
||||
Unit=monitor-update-apply-$COMPONENT.service
|
||||
PathChanged=$DATA_DIR/update/$watched
|
||||
Unit=monitor-update-$action-$COMPONENT.service
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
WantedBy=$TARGET
|
||||
UNITFILE
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now "monitor-update-check-$COMPONENT.path" "monitor-update-apply-$COMPONENT.path"
|
||||
echo "updater installed for $COMPONENT (policy: $POLICY)"
|
||||
done
|
||||
fi
|
||||
if [ "$ENABLE" = true ]; then
|
||||
service_ctl daemon-reload
|
||||
if [ -n "$DATA_VOLUME" ]; then
|
||||
service_ctl disable --now "monitor-update-check-$COMPONENT.path" "monitor-update-apply-$COMPONENT.path" >/dev/null 2>&1 || true
|
||||
service_ctl enable --now "monitor-update-$COMPONENT.timer"
|
||||
else
|
||||
service_ctl disable --now "monitor-update-$COMPONENT.timer" >/dev/null 2>&1 || true
|
||||
service_ctl enable --now "monitor-update-check-$COMPONENT.path" "monitor-update-apply-$COMPONENT.path"
|
||||
fi
|
||||
fi
|
||||
echo "updater installed for $COMPONENT (engine: $ENGINE, scope: $SERVICE_SCOPE, policy: $POLICY)"
|
||||
echo "container service must read IMAGE from $ENV_FILE"
|
||||
@@ -7,6 +7,8 @@
|
||||
# - 镜像仓库、组件、服务名都来自本机的配置(/etc/monitor-update/<组件>.conf),清单里写什么都不会改变拉哪个仓库。
|
||||
# - 清单的 seq 必须比已安装的大(防止旧清单被重放回滚)。
|
||||
# - 业务进程(Agent、Probe)只能往数据目录里写「清单」和「请求」文件,更新由这个宿主机脚本完成,业务进程不执行任何命令。
|
||||
# Shell snippets below execute inside helper containers.
|
||||
# shellcheck disable=SC2016
|
||||
set -eu
|
||||
|
||||
CMD="${1:-}"
|
||||
@@ -14,24 +16,63 @@ COMPONENT="${2:-}"
|
||||
case "$CMD" in check|apply) ;; *) echo "usage: monitor-update <check|apply> <agent|probe>" >&2; exit 2 ;; esac
|
||||
case "$COMPONENT" in agent|probe) ;; *) echo "component must be agent or probe" >&2; exit 2 ;; esac
|
||||
|
||||
CONF="${MONITOR_UPDATE_CONF_DIR:-/etc/monitor-update}/$COMPONENT.conf"
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
DEFAULT_CONF_DIR=/etc/monitor-update
|
||||
else
|
||||
DEFAULT_CONF_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/monitor"
|
||||
fi
|
||||
CONF="${MONITOR_UPDATE_CONF_DIR:-$DEFAULT_CONF_DIR}/$COMPONENT.conf"
|
||||
[ -r "$CONF" ] || { echo "missing $CONF" >&2; exit 2; }
|
||||
# 配置:DATA_DIR(容器里 /etc/monitor 对应的宿主机目录)、IMAGE_REPO、UNIT、PUBKEY、POLICY(auto|manual)、AUTHFILE(可选)、
|
||||
# STATE_DIR(已安装版本的记录)、ENV_FILE(容器单元读取的 IMAGE=… 文件)
|
||||
# shellcheck disable=SC1090
|
||||
. "$CONF"
|
||||
: "${DATA_DIR:?}" "${IMAGE_REPO:?}" "${UNIT:?}" "${PUBKEY:?}" "${ENV_FILE:?}"
|
||||
: "${IMAGE_REPO:?}" "${UNIT:?}" "${PUBKEY:?}" "${ENV_FILE:?}"
|
||||
POLICY="${POLICY:-manual}"
|
||||
STATE_DIR="${STATE_DIR:-/var/lib/monitor-update}"
|
||||
ENGINE="${ENGINE:-podman}"
|
||||
SERVICE_SCOPE="${SERVICE_SCOPE:-system}"
|
||||
DATA_VOLUME="${DATA_VOLUME:-}"
|
||||
STATUS_VOLUME="${STATUS_VOLUME:-monitor-$COMPONENT-status}"
|
||||
case "$ENGINE" in podman|docker) ;; *) echo "unsupported engine: $ENGINE" >&2; exit 2 ;; esac
|
||||
case "$SERVICE_SCOPE" in system|user) ;; *) echo "unsupported service scope: $SERVICE_SCOPE" >&2; exit 2 ;; esac
|
||||
service_ctl() {
|
||||
if [ "$SERVICE_SCOPE" = user ]; then systemctl --user "$@"; else systemctl "$@"; fi
|
||||
}
|
||||
pull_image() {
|
||||
if [ -n "${AUTHFILE:-}" ]; then
|
||||
if [ "$ENGINE" = podman ]; then
|
||||
"$ENGINE" pull --quiet --authfile "$AUTHFILE" "$1"
|
||||
else
|
||||
mkdir -m 700 "$WORK/docker-auth"
|
||||
cp "$AUTHFILE" "$WORK/docker-auth/config.json"
|
||||
DOCKER_CONFIG="$WORK/docker-auth" "$ENGINE" pull --quiet "$1"
|
||||
fi
|
||||
else
|
||||
"$ENGINE" pull --quiet "$1"
|
||||
fi
|
||||
}
|
||||
# 业务进程(容器里的 Agent、Probe)可写的目录只有 update/:它在里面放 offer.manifest、offer.sig、apply.request。
|
||||
# 这个脚本以 root 运行,所以:只读取普通文件(不跟符号链接)、读取之后先复制到自己的临时目录再验证和使用(防止验证之后被换掉),
|
||||
# 并且绝不往 update/ 里写任何东西;状态写在 root 拥有的 update-status/ 里(业务进程只读)。
|
||||
UPDATE_DIR="$DATA_DIR/update"
|
||||
STATUS_DIR="$DATA_DIR/update-status"
|
||||
STATUS="$STATUS_DIR/status.json"
|
||||
# 更新器在容器外运行:只读取普通文件(不跟符号链接)、读取之后先复制到自己的临时目录再验证和使用(防止验证之后被换掉),
|
||||
# 状态目录在 rootless 容器里另行只读挂载;更新器配置和已安装版本状态不挂入容器。
|
||||
if [ -n "$DATA_VOLUME" ]; then
|
||||
# Read/write volume contents via short-lived engine-managed helpers, without host mountpoint access.
|
||||
: "${HELPER_IMAGE:?}"
|
||||
"$ENGINE" volume inspect "$DATA_VOLUME" >/dev/null
|
||||
"$ENGINE" volume inspect "$STATUS_VOLUME" >/dev/null
|
||||
else
|
||||
: "${DATA_DIR:?}"
|
||||
UPDATE_DIR="$DATA_DIR/update"
|
||||
STATUS_DIR="$DATA_DIR/update-status"
|
||||
STATUS="$STATUS_DIR/status.json"
|
||||
mkdir -p "$STATUS_DIR"
|
||||
chmod 755 "$STATUS_DIR"
|
||||
fi
|
||||
INSTALLED="$STATE_DIR/$COMPONENT.installed" # 两行:seq、digest
|
||||
mkdir -p "$STATE_DIR" "$STATUS_DIR"
|
||||
chmod 755 "$STATUS_DIR"
|
||||
mkdir -p "$STATE_DIR"
|
||||
# Serialize check/apply path events for this component.
|
||||
exec 9>"$STATE_DIR/$COMPONENT.lock"
|
||||
flock -w 290 9 || { echo "another update is still running" >&2; exit 1; }
|
||||
WORK="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK"' EXIT
|
||||
OFFER_MANIFEST="$WORK/offer.manifest"
|
||||
@@ -41,6 +82,12 @@ OFFER_SIG="$WORK/offer.sig"
|
||||
fetch_offer() {
|
||||
rm -f "$OFFER_MANIFEST" "$OFFER_SIG"
|
||||
for name in offer.manifest offer.sig; do
|
||||
if [ -n "$DATA_VOLUME" ]; then
|
||||
"$ENGINE" run --pull=never --rm --network none --read-only --cap-drop ALL --security-opt no-new-privileges \
|
||||
-v "$DATA_VOLUME:/etc/monitor:ro" --entrypoint /bin/sh "$HELPER_IMAGE" \
|
||||
-c 'f="/etc/monitor/update/$1"; if [ -f "$f" ] && [ ! -L "$f" ]; then head -c 8192 "$f"; fi' sh "$name" > "$WORK/$name"
|
||||
continue
|
||||
fi
|
||||
f="$UPDATE_DIR/$name"
|
||||
if [ -f "$f" ] && [ ! -L "$f" ]; then head -c 8192 "$f" > "$WORK/$name"; fi
|
||||
done
|
||||
@@ -48,13 +95,22 @@ fetch_offer() {
|
||||
|
||||
now() { date +%s; }
|
||||
|
||||
# 状态文件:Agent / Probe 读它并随心跳报给面板。root 写,业务进程只读。
|
||||
# 状态文件:更新器写,Agent / Probe 从只读挂载中读取并随心跳上报。
|
||||
write_status() { # state available_json error
|
||||
tmp="$(mktemp "$STATUS_DIR/.status.XXXXXX")"
|
||||
tmp="$WORK/status.json"
|
||||
printf '{"state":"%s","current":"%s","available":%s,"error":"%s","at":%s}\n' \
|
||||
"$1" "$(current_digest)" "${2:-null}" "$(printf '%s' "${3:-}" | tr -d '"\\\n' | cut -c1-200)" "$(now)" > "$tmp"
|
||||
chmod 644 "$tmp"
|
||||
mv -f "$tmp" "$STATUS"
|
||||
if [ -n "$DATA_VOLUME" ]; then
|
||||
"$ENGINE" run --pull=never --rm -i --user 0 --network none --read-only --cap-drop ALL --security-opt no-new-privileges \
|
||||
-v "$STATUS_VOLUME:/status" --entrypoint /bin/sh "$HELPER_IMAGE" \
|
||||
-c 'tmp=$(mktemp /status/.status.XXXXXX); cat > "$tmp"; chmod 644 "$tmp"; mv -f "$tmp" /status/status.json' < "$tmp"
|
||||
else
|
||||
chmod 644 "$tmp"
|
||||
destination="$(mktemp "$STATUS_DIR/.status.XXXXXX")"
|
||||
cp "$tmp" "$destination"
|
||||
chmod 644 "$destination"
|
||||
mv -f "$destination" "$STATUS"
|
||||
fi
|
||||
}
|
||||
|
||||
current_digest() { [ -r "$INSTALLED" ] && sed -n 2p "$INSTALLED" || echo ""; }
|
||||
@@ -82,7 +138,7 @@ verify_offer() {
|
||||
[ "$M_SEQ" -gt "$(installed_seq)" ] || { ERR="stale manifest (seq $M_SEQ <= $(installed_seq))"; return 1; }
|
||||
}
|
||||
|
||||
available_json() { printf '{"version":"%s","tag":"%s","digest":"%s","seq":%s}' "$(printf '%s' "$M_VERSION" | tr -d '"\\')" "$(printf '%s' "$M_TAG" | tr -d '"\\')" "$M_DIGEST" "$M_SEQ"; }
|
||||
available_json() { printf '{"version":"%s","tag":"%s","digest":"%s","seq":%s}' "$(printf '%s' "$M_VERSION" | tr -d '\042\134')" "$(printf '%s' "$M_TAG" | tr -d '\042\134')" "$M_DIGEST" "$M_SEQ"; }
|
||||
|
||||
do_check() {
|
||||
fetch_offer
|
||||
@@ -105,25 +161,29 @@ do_check() {
|
||||
}
|
||||
|
||||
do_apply() {
|
||||
rm -f "$UPDATE_DIR/apply.request"
|
||||
if [ -n "$DATA_VOLUME" ]; then
|
||||
"$ENGINE" run --pull=never --rm --network none --read-only --cap-drop ALL --security-opt no-new-privileges \
|
||||
-v "$DATA_VOLUME:/etc/monitor" --entrypoint /bin/sh "$HELPER_IMAGE" -c 'rm -f /etc/monitor/update/apply.request'
|
||||
else
|
||||
rm -f "$UPDATE_DIR/apply.request"
|
||||
fi
|
||||
fetch_offer
|
||||
verify_offer || { write_status invalid null "$ERR"; echo "refusing to apply: $ERR" >&2; return 1; }
|
||||
avail="$(available_json)"
|
||||
write_status applying "$avail" ""
|
||||
ref="$IMAGE_REPO@$M_DIGEST"
|
||||
if [ -n "${AUTHFILE:-}" ]; then export REGISTRY_AUTH_FILE="$AUTHFILE"; fi
|
||||
if ! podman pull --quiet "$ref" >/dev/null 2>&1; then
|
||||
if ! pull_image "$ref" >/dev/null 2>&1; then
|
||||
write_status failed "$avail" "pull failed"; echo "pull failed: $ref" >&2; return 1
|
||||
fi
|
||||
prev="$(cat "$ENV_FILE" 2>/dev/null || true)"
|
||||
printf 'IMAGE=%s\n' "$ref" > "$ENV_FILE.new" && mv -f "$ENV_FILE.new" "$ENV_FILE"
|
||||
systemctl restart "$UNIT" || true
|
||||
service_ctl restart "$UNIT" || true
|
||||
# 健康确认:服务在跑,并且 20 秒内没有再被重启(崩溃循环算失败)
|
||||
sleep 5
|
||||
before="$(systemctl show -p NRestarts --value "$UNIT" 2>/dev/null || echo 0)"
|
||||
before="$(service_ctl show -p NRestarts --value "$UNIT" 2>/dev/null || echo 0)"
|
||||
sleep 15
|
||||
after="$(systemctl show -p NRestarts --value "$UNIT" 2>/dev/null || echo 0)"
|
||||
if systemctl is-active --quiet "$UNIT" && [ "$before" = "$after" ]; then
|
||||
after="$(service_ctl show -p NRestarts --value "$UNIT" 2>/dev/null || echo 0)"
|
||||
if service_ctl is-active --quiet "$UNIT" && [ "$before" = "$after" ]; then
|
||||
printf '%s\n%s\n' "$M_SEQ" "$M_DIGEST" > "$INSTALLED"
|
||||
write_status up_to_date null ""
|
||||
echo "updated to $M_TAG"
|
||||
@@ -131,12 +191,20 @@ do_apply() {
|
||||
fi
|
||||
echo "new version did not become healthy, rolling back" >&2
|
||||
if [ -n "$prev" ]; then printf '%s\n' "$prev" > "$ENV_FILE"; fi
|
||||
systemctl restart "$UNIT" || true
|
||||
service_ctl restart "$UNIT" || true
|
||||
write_status rolled_back "$avail" "new version did not become healthy"
|
||||
return 1
|
||||
}
|
||||
|
||||
case "$CMD" in
|
||||
check) do_check ;;
|
||||
check)
|
||||
if [ -n "$DATA_VOLUME" ] && [ "$POLICY" != auto ]; then
|
||||
requested="$($ENGINE run --pull=never --rm --network none --read-only --cap-drop ALL --security-opt no-new-privileges \
|
||||
-v "$DATA_VOLUME:/etc/monitor:ro" --entrypoint /bin/sh "$HELPER_IMAGE" \
|
||||
-c '[ ! -f /etc/monitor/update/apply.request ] || printf yes')"
|
||||
if [ "$requested" = yes ]; then do_apply; else do_check; fi
|
||||
else
|
||||
do_check
|
||||
fi ;;
|
||||
apply) do_apply ;;
|
||||
esac
|
||||
Reference in new issue
Block a user