@rizom/ops 0.2.0-alpha.255 → 0.2.0-alpha.256

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rizom/ops",
3
- "version": "0.2.0-alpha.255",
3
+ "version": "0.2.0-alpha.256",
4
4
  "description": "Operator CLI for managing private brain fleet registry repos",
5
5
  "keywords": [
6
6
  "brains",
@@ -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
+ }