@rizom/ops 0.2.0-alpha.255 → 0.2.0-alpha.257
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -221,6 +221,13 @@ jobs:
|
|
|
221
221
|
BRAIN_YAML_PATH: ${{ steps.user_config.outputs.brain_yaml_path }}
|
|
222
222
|
run: kamal setup --skip-push -c deploy/kamal/deploy.yml
|
|
223
223
|
|
|
224
|
+
- name: Install container health watchdog
|
|
225
|
+
env:
|
|
226
|
+
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
227
|
+
run: |
|
|
228
|
+
SSH_USER="$(ruby -e 'require "yaml"; config = YAML.load_file("deploy/kamal/deploy.yml") || {}; puts(config.dig("ssh", "user") || "root")')" \
|
|
229
|
+
bun deploy/scripts/install-health-watchdog.ts
|
|
230
|
+
|
|
224
231
|
- name: Verify origin TLS
|
|
225
232
|
env:
|
|
226
233
|
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { requireEnv } from "./helpers";
|
|
2
|
+
|
|
3
|
+
const WATCHDOG_PATH = "/usr/local/sbin/brains-health-watchdog";
|
|
4
|
+
const SERVICE_PATH = "/etc/systemd/system/brains-health-watchdog.service";
|
|
5
|
+
const TIMER_PATH = "/etc/systemd/system/brains-health-watchdog.timer";
|
|
6
|
+
|
|
7
|
+
export const healthWatchdogScript: string = `#!/usr/bin/env bash
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
umask 077
|
|
10
|
+
|
|
11
|
+
lock_path="\${BRAIN_WATCHDOG_LOCK_PATH:-/run/brains-health-watchdog.lock}"
|
|
12
|
+
exec 9>"$lock_path"
|
|
13
|
+
flock -n 9 || exit 0
|
|
14
|
+
|
|
15
|
+
incident_dir="\${BRAIN_WATCHDOG_INCIDENT_DIR:-/var/log/brains-health-watchdog}"
|
|
16
|
+
state_dir="\${BRAIN_WATCHDOG_STATE_DIR:-/var/lib/brains-health-watchdog}"
|
|
17
|
+
MAX_RESTARTS=3
|
|
18
|
+
WINDOW_SECONDS=3600
|
|
19
|
+
mkdir -p "$incident_dir" "$state_dir"
|
|
20
|
+
|
|
21
|
+
write_safe_inspect() {
|
|
22
|
+
docker inspect --format '{"id":{{json .Id}},"name":{{json .Name}},"image":{{json .Config.Image}},"state":{{json .State}},"restartPolicy":{{json .HostConfig.RestartPolicy}}}' "$1"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
while read -r container_id; do
|
|
26
|
+
[ -n "$container_id" ] || continue
|
|
27
|
+
service_name=$(docker inspect --format '{{ index .Config.Labels "service" }}' "$container_id" 2>/dev/null || true)
|
|
28
|
+
container_name=$(docker inspect --format '{{ .Name }}' "$container_id" 2>/dev/null | tr -d '/' || true)
|
|
29
|
+
state_key=$(printf '%s-%s' "$service_name" "$container_name" | tr -c 'A-Za-z0-9_.-' '_')
|
|
30
|
+
state_path="$state_dir/$state_key.restarts"
|
|
31
|
+
recent_path="$state_path.tmp"
|
|
32
|
+
now_epoch=$(date -u +%s)
|
|
33
|
+
cutoff_epoch=$((now_epoch - WINDOW_SECONDS))
|
|
34
|
+
touch "$state_path"
|
|
35
|
+
awk -v cutoff="$cutoff_epoch" '$1 >= cutoff' "$state_path" >"$recent_path"
|
|
36
|
+
restart_count=$(wc -l <"$recent_path")
|
|
37
|
+
|
|
38
|
+
timestamp=$(date -u +%Y%m%dT%H%M%SZ)
|
|
39
|
+
incident_path="$incident_dir/$timestamp-$state_key.log"
|
|
40
|
+
temporary_path="$incident_path.tmp"
|
|
41
|
+
|
|
42
|
+
if [ "$restart_count" -ge "$MAX_RESTARTS" ]; then
|
|
43
|
+
{
|
|
44
|
+
printf '{"timestamp":"%s","event":"restart-suppressed","reason":"restart-budget-exhausted","service":"%s","container":"%s","restartsInWindow":%s}\\n' "$timestamp" "$service_name" "$container_name" "$restart_count"
|
|
45
|
+
write_safe_inspect "$container_id"
|
|
46
|
+
docker logs --timestamps --tail 1000 "$container_id"
|
|
47
|
+
} >"$temporary_path" 2>&1
|
|
48
|
+
mv "$temporary_path" "$incident_path"
|
|
49
|
+
mv "$recent_path" "$state_path"
|
|
50
|
+
logger -t brains-health-watchdog "restart budget exhausted container=$container_name service=$service_name incident=$incident_path"
|
|
51
|
+
continue
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
{
|
|
55
|
+
printf '{"timestamp":"%s","event":"container-restart","reason":"docker-health-unhealthy","service":"%s","container":"%s"}\\n' "$timestamp" "$service_name" "$container_name"
|
|
56
|
+
write_safe_inspect "$container_id"
|
|
57
|
+
docker logs --timestamps --tail 1000 "$container_id"
|
|
58
|
+
} >"$temporary_path" 2>&1
|
|
59
|
+
mv "$temporary_path" "$incident_path"
|
|
60
|
+
printf '%s\\n' "$now_epoch" >>"$recent_path"
|
|
61
|
+
mv "$recent_path" "$state_path"
|
|
62
|
+
|
|
63
|
+
logger -t brains-health-watchdog "restarting unhealthy container=$container_name service=$service_name incident=$incident_path"
|
|
64
|
+
docker restart "$container_id"
|
|
65
|
+
done < <(docker ps --filter health=unhealthy --filter label=service --format '{{.ID}}')
|
|
66
|
+
`;
|
|
67
|
+
|
|
68
|
+
export const healthWatchdogServiceUnit: string = `[Unit]
|
|
69
|
+
Description=Restart persistently unhealthy Brain containers
|
|
70
|
+
After=docker.service
|
|
71
|
+
Requires=docker.service
|
|
72
|
+
|
|
73
|
+
[Service]
|
|
74
|
+
Type=oneshot
|
|
75
|
+
ExecStart=${WATCHDOG_PATH}
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
export const healthWatchdogTimerUnit: string = `[Unit]
|
|
79
|
+
Description=Monitor Brain container liveness
|
|
80
|
+
|
|
81
|
+
[Timer]
|
|
82
|
+
OnBootSec=2min
|
|
83
|
+
OnUnitActiveSec=30s
|
|
84
|
+
RandomizedDelaySec=5s
|
|
85
|
+
Persistent=true
|
|
86
|
+
Unit=brains-health-watchdog.service
|
|
87
|
+
|
|
88
|
+
[Install]
|
|
89
|
+
WantedBy=timers.target
|
|
90
|
+
`;
|
|
91
|
+
|
|
92
|
+
function installCommand(path: string, content: string, mode: string): string {
|
|
93
|
+
const encoded = Buffer.from(content, "utf8").toString("base64");
|
|
94
|
+
return `printf '%s' '${encoded}' | $SUDO base64 --decode | $SUDO tee '${path}' >/dev/null\n$SUDO chmod ${mode} '${path}'`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function buildHealthWatchdogInstallScript(): string {
|
|
98
|
+
return `#!/usr/bin/env bash
|
|
99
|
+
set -euo pipefail
|
|
100
|
+
SUDO=""
|
|
101
|
+
if [ "$(id -u)" -ne 0 ]; then
|
|
102
|
+
SUDO=sudo
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
${installCommand(WATCHDOG_PATH, healthWatchdogScript, "0755")}
|
|
106
|
+
${installCommand(SERVICE_PATH, healthWatchdogServiceUnit, "0644")}
|
|
107
|
+
${installCommand(TIMER_PATH, healthWatchdogTimerUnit, "0644")}
|
|
108
|
+
|
|
109
|
+
$SUDO systemctl daemon-reload
|
|
110
|
+
$SUDO systemctl enable --now brains-health-watchdog.timer
|
|
111
|
+
`;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function installHealthWatchdog(): Promise<void> {
|
|
115
|
+
const serverIp = requireEnv("SERVER_IP");
|
|
116
|
+
const sshUser = requireEnv("SSH_USER").trim();
|
|
117
|
+
if (!sshUser) throw new Error("Missing SSH_USER");
|
|
118
|
+
const child = Bun.spawn(["ssh", `${sshUser}@${serverIp}`, "bash -s"], {
|
|
119
|
+
stdin: new Blob([buildHealthWatchdogInstallScript()]),
|
|
120
|
+
stdout: "inherit",
|
|
121
|
+
stderr: "inherit",
|
|
122
|
+
});
|
|
123
|
+
const exitCode = await child.exited;
|
|
124
|
+
if (exitCode !== 0) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Health watchdog installation failed with exit ${exitCode}`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (import.meta.main) {
|
|
132
|
+
await installHealthWatchdog();
|
|
133
|
+
}
|