@glassmkr/crucible 0.7.1 → 0.8.0
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/dist/alerts/__tests__/rules.test.d.ts +1 -0
- package/dist/alerts/__tests__/rules.test.js +325 -0
- package/dist/alerts/__tests__/rules.test.js.map +1 -0
- package/dist/alerts/rules.d.ts +8 -0
- package/dist/alerts/rules.js +139 -32
- package/dist/alerts/rules.js.map +1 -1
- package/dist/api.d.ts +2 -0
- package/dist/api.js +7 -0
- package/dist/api.js.map +1 -0
- package/dist/collect/__tests__/dmi.test.d.ts +1 -0
- package/dist/collect/__tests__/dmi.test.js +114 -0
- package/dist/collect/__tests__/dmi.test.js.map +1 -0
- package/dist/collect/__tests__/ipmi.test.js +47 -1
- package/dist/collect/__tests__/ipmi.test.js.map +1 -1
- package/dist/collect/__tests__/thermal.test.d.ts +1 -0
- package/dist/collect/__tests__/thermal.test.js +164 -0
- package/dist/collect/__tests__/thermal.test.js.map +1 -0
- package/dist/collect/dmi.d.ts +19 -0
- package/dist/collect/dmi.js +109 -0
- package/dist/collect/dmi.js.map +1 -0
- package/dist/collect/ipmi.d.ts +27 -2
- package/dist/collect/ipmi.js +90 -2
- package/dist/collect/ipmi.js.map +1 -1
- package/dist/collect/thermal.d.ts +10 -0
- package/dist/collect/thermal.js +187 -0
- package/dist/collect/thermal.js.map +1 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/index.js +51 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/__tests__/capability.test.d.ts +1 -0
- package/dist/lib/__tests__/capability.test.js +87 -0
- package/dist/lib/__tests__/capability.test.js.map +1 -0
- package/dist/lib/__tests__/vendor-sensors.test.d.ts +1 -0
- package/dist/lib/__tests__/vendor-sensors.test.js +49 -0
- package/dist/lib/__tests__/vendor-sensors.test.js.map +1 -0
- package/dist/lib/capability.d.ts +21 -0
- package/dist/lib/capability.js +110 -0
- package/dist/lib/capability.js.map +1 -0
- package/dist/lib/cpu-thermal-chips.d.ts +2 -0
- package/dist/lib/cpu-thermal-chips.js +28 -0
- package/dist/lib/cpu-thermal-chips.js.map +1 -0
- package/dist/lib/types.d.ts +58 -0
- package/dist/lib/vendor-sensors.d.ts +27 -0
- package/dist/lib/vendor-sensors.js +63 -0
- package/dist/lib/vendor-sensors.js.map +1 -0
- package/dist/notify/telegram.js +1 -1
- package/dist/notify/telegram.js.map +1 -1
- package/package.json +16 -1
- package/rule-ids.json +29 -0
- package/.dockerignore +0 -13
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -24
- package/.github/ISSUE_TEMPLATE/no_data.md +0 -26
- package/.github/workflows/docker.yml +0 -53
- package/.github/workflows/publish.yml +0 -25
- package/Dockerfile +0 -59
- package/config/collector.example.yaml +0 -43
- package/docker-compose.yml +0 -26
- package/scripts/sign-release.sh +0 -29
- package/src/__tests__/cli.test.ts +0 -74
- package/src/__tests__/reboot-marker.test.ts +0 -122
- package/src/alerts/evaluator.ts +0 -15
- package/src/alerts/rules.ts +0 -283
- package/src/alerts/state.ts +0 -92
- package/src/cli.ts +0 -112
- package/src/collect/__tests__/ipmi.test.ts +0 -96
- package/src/collect/__tests__/smart.test.ts +0 -68
- package/src/collect/__tests__/system.test.ts +0 -29
- package/src/collect/__tests__/zfs.test.ts +0 -72
- package/src/collect/conntrack.ts +0 -27
- package/src/collect/cpu.ts +0 -92
- package/src/collect/disks.ts +0 -91
- package/src/collect/fd.ts +0 -31
- package/src/collect/io-errors.ts +0 -23
- package/src/collect/io-latency.ts +0 -103
- package/src/collect/ipmi.ts +0 -207
- package/src/collect/memory.ts +0 -30
- package/src/collect/network.ts +0 -193
- package/src/collect/ntp.ts +0 -114
- package/src/collect/os-alerts.ts +0 -43
- package/src/collect/raid.ts +0 -40
- package/src/collect/security.ts +0 -268
- package/src/collect/smart.ts +0 -72
- package/src/collect/system.ts +0 -32
- package/src/collect/systemd.ts +0 -33
- package/src/collect/zfs.ts +0 -66
- package/src/config.ts +0 -65
- package/src/index.ts +0 -221
- package/src/lib/__tests__/parse.test.ts +0 -28
- package/src/lib/exec.ts +0 -16
- package/src/lib/parse.ts +0 -29
- package/src/lib/reboot-marker.ts +0 -88
- package/src/lib/types.ts +0 -226
- package/src/lib/version-check.ts +0 -39
- package/src/lib/version.ts +0 -33
- package/src/metrics-server.ts +0 -123
- package/src/notify/email.ts +0 -69
- package/src/notify/slack.ts +0 -47
- package/src/notify/telegram.ts +0 -65
- package/src/push/forge.ts +0 -109
- package/tsconfig.json +0 -15
- package/vitest.config.ts +0 -12
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: No Data in Forge
|
|
3
|
-
about: Crucible is running but Forge shows no data
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
**Crucible version:**
|
|
7
|
-
**OS and version:**
|
|
8
|
-
**How long since install:**
|
|
9
|
-
|
|
10
|
-
**Can you reach Forge?**
|
|
11
|
-
```
|
|
12
|
-
curl -s -o /dev/null -w "%{http_code}" https://forge.glassmkr.com/api/health
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
**Service status:**
|
|
16
|
-
```
|
|
17
|
-
systemctl status glassmkr-crucible
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
**Last 50 log lines:**
|
|
21
|
-
```
|
|
22
|
-
journalctl -u glassmkr-crucible -n 50 --no-pager
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
**Is smartctl installed?** `which smartctl`
|
|
26
|
-
**Is ipmitool installed?** `which ipmitool`
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
name: Build and publish Docker image
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
tags: ['v*']
|
|
7
|
-
|
|
8
|
-
permissions:
|
|
9
|
-
contents: read
|
|
10
|
-
packages: write
|
|
11
|
-
|
|
12
|
-
jobs:
|
|
13
|
-
docker:
|
|
14
|
-
name: Build and push to ghcr.io and docker.io
|
|
15
|
-
runs-on: ubuntu-latest
|
|
16
|
-
steps:
|
|
17
|
-
- uses: actions/checkout@v4
|
|
18
|
-
|
|
19
|
-
- uses: docker/setup-buildx-action@v3
|
|
20
|
-
|
|
21
|
-
- uses: docker/login-action@v3
|
|
22
|
-
with:
|
|
23
|
-
registry: ghcr.io
|
|
24
|
-
username: ${{ github.actor }}
|
|
25
|
-
password: ${{ secrets.GITHUB_TOKEN }}
|
|
26
|
-
|
|
27
|
-
- uses: docker/login-action@v3
|
|
28
|
-
with:
|
|
29
|
-
registry: docker.io
|
|
30
|
-
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
31
|
-
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
32
|
-
|
|
33
|
-
- uses: docker/metadata-action@v5
|
|
34
|
-
id: meta
|
|
35
|
-
with:
|
|
36
|
-
images: |
|
|
37
|
-
ghcr.io/glassmkr/crucible
|
|
38
|
-
docker.io/glassmkr/crucible
|
|
39
|
-
tags: |
|
|
40
|
-
type=semver,pattern={{version}}
|
|
41
|
-
type=semver,pattern={{major}}.{{minor}}
|
|
42
|
-
type=raw,value=latest,enable={{is_default_branch}}
|
|
43
|
-
type=sha,prefix=sha-,format=short
|
|
44
|
-
|
|
45
|
-
- uses: docker/build-push-action@v6
|
|
46
|
-
with:
|
|
47
|
-
context: .
|
|
48
|
-
platforms: linux/amd64
|
|
49
|
-
push: true
|
|
50
|
-
tags: ${{ steps.meta.outputs.tags }}
|
|
51
|
-
labels: ${{ steps.meta.outputs.labels }}
|
|
52
|
-
cache-from: type=gha
|
|
53
|
-
cache-to: type=gha,mode=max
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
name: Publish to npm
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
tags: ['v*']
|
|
6
|
-
|
|
7
|
-
permissions:
|
|
8
|
-
contents: read
|
|
9
|
-
id-token: write
|
|
10
|
-
|
|
11
|
-
jobs:
|
|
12
|
-
publish:
|
|
13
|
-
name: npm publish
|
|
14
|
-
runs-on: ubuntu-latest
|
|
15
|
-
steps:
|
|
16
|
-
- uses: actions/checkout@v4
|
|
17
|
-
- uses: actions/setup-node@v4
|
|
18
|
-
with:
|
|
19
|
-
# Node 24 ships with npm 11.x which has working npm Trusted Publishing.
|
|
20
|
-
node-version: 24
|
|
21
|
-
registry-url: 'https://registry.npmjs.org'
|
|
22
|
-
- run: npm --version
|
|
23
|
-
- run: npm install
|
|
24
|
-
- run: npm run build
|
|
25
|
-
- run: npm publish --access public --provenance
|
package/Dockerfile
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
# syntax=docker/dockerfile:1.7
|
|
2
|
-
# Multi-stage build for Glassmkr Crucible monitoring agent.
|
|
3
|
-
|
|
4
|
-
# ---------- Stage 1: build TypeScript to dist/ ----------
|
|
5
|
-
FROM node:24-slim AS builder
|
|
6
|
-
WORKDIR /build
|
|
7
|
-
COPY package.json package-lock.json* ./
|
|
8
|
-
RUN npm install --include=dev --no-audit --no-fund
|
|
9
|
-
COPY tsconfig.json ./
|
|
10
|
-
COPY src ./src
|
|
11
|
-
RUN npm run build
|
|
12
|
-
|
|
13
|
-
# ---------- Stage 2: production runtime ----------
|
|
14
|
-
FROM node:24-slim AS runtime
|
|
15
|
-
|
|
16
|
-
# Hardware monitoring tools. Crucible shells out to these; they must be on PATH.
|
|
17
|
-
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
18
|
-
smartmontools \
|
|
19
|
-
ipmitool \
|
|
20
|
-
dmidecode \
|
|
21
|
-
lm-sensors \
|
|
22
|
-
ethtool \
|
|
23
|
-
util-linux \
|
|
24
|
-
procps \
|
|
25
|
-
net-tools \
|
|
26
|
-
iproute2 \
|
|
27
|
-
ca-certificates \
|
|
28
|
-
&& rm -rf /var/lib/apt/lists/*
|
|
29
|
-
|
|
30
|
-
WORKDIR /app
|
|
31
|
-
|
|
32
|
-
# Production node_modules only.
|
|
33
|
-
COPY package.json package-lock.json* ./
|
|
34
|
-
RUN npm ci --omit=dev --no-audit --no-fund && npm cache clean --force
|
|
35
|
-
|
|
36
|
-
# Built code.
|
|
37
|
-
COPY --from=builder /build/dist ./dist
|
|
38
|
-
|
|
39
|
-
# Create a non-root user for future use. IPMI and SMART typically require root,
|
|
40
|
-
# so the container is expected to run with --privileged or cap_add DAC_READ_SEARCH etc.
|
|
41
|
-
# Keeping the user available lets operators drop privileges when hardware access is not needed.
|
|
42
|
-
RUN useradd --system --no-create-home --shell /usr/sbin/nologin glassmkr
|
|
43
|
-
|
|
44
|
-
# Crucible reads /etc/glassmkr/collector.yaml by default.
|
|
45
|
-
# Mount the host config directory at this path.
|
|
46
|
-
RUN mkdir -p /etc/glassmkr
|
|
47
|
-
|
|
48
|
-
# Container health: verify the Node process is actually running and hasn't crashed.
|
|
49
|
-
HEALTHCHECK --interval=60s --timeout=10s --start-period=30s --retries=3 \
|
|
50
|
-
CMD pgrep -f "node /app/dist/index.js" > /dev/null || exit 1
|
|
51
|
-
|
|
52
|
-
LABEL org.opencontainers.image.source="https://github.com/glassmkr/crucible" \
|
|
53
|
-
org.opencontainers.image.description="Glassmkr Crucible - bare metal server monitoring agent" \
|
|
54
|
-
org.opencontainers.image.licenses="MIT" \
|
|
55
|
-
org.opencontainers.image.title="Crucible" \
|
|
56
|
-
org.opencontainers.image.vendor="Glassmkr"
|
|
57
|
-
|
|
58
|
-
# Crucible does not listen on any port; data flows outbound to Forge.
|
|
59
|
-
ENTRYPOINT ["node", "/app/dist/index.js"]
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
# Glassmkr Crucible Configuration
|
|
2
|
-
# Copy to /etc/glassmkr/crucible.yaml
|
|
3
|
-
|
|
4
|
-
# Server identity
|
|
5
|
-
server_name: "my-server"
|
|
6
|
-
|
|
7
|
-
# Collection settings
|
|
8
|
-
collection:
|
|
9
|
-
interval_seconds: 300 # How often to collect (default 5 minutes)
|
|
10
|
-
ipmi: true # Collect IPMI data (requires ipmitool)
|
|
11
|
-
smart: true # Collect SMART data (requires smartmontools)
|
|
12
|
-
|
|
13
|
-
# Forge integration (optional dashboard)
|
|
14
|
-
forge:
|
|
15
|
-
enabled: false
|
|
16
|
-
url: "https://forge.glassmkr.com"
|
|
17
|
-
api_key: "" # Get this from forge.glassmkr.com after registering a server
|
|
18
|
-
|
|
19
|
-
# Alert thresholds (all optional, sensible defaults used if omitted)
|
|
20
|
-
thresholds:
|
|
21
|
-
ram_percent: 90 # Alert when RAM usage exceeds this
|
|
22
|
-
swap_alert: true # Alert on any swap usage
|
|
23
|
-
disk_percent: 85 # Alert when any disk exceeds this
|
|
24
|
-
iowait_percent: 20 # Alert when CPU iowait exceeds this
|
|
25
|
-
nvme_wear_percent: 85 # Alert when NVMe lifetime wear exceeds this
|
|
26
|
-
disk_latency_nvme_ms: 50 # p99 latency threshold for NVMe
|
|
27
|
-
disk_latency_hdd_ms: 200 # p99 latency threshold for HDD
|
|
28
|
-
cpu_temp_warning_c: 80 # CPU temperature warning
|
|
29
|
-
cpu_temp_critical_c: 90 # CPU temperature critical
|
|
30
|
-
interface_utilization_percent: 90 # Network saturation threshold
|
|
31
|
-
|
|
32
|
-
# Notification channels (all optional)
|
|
33
|
-
channels:
|
|
34
|
-
telegram:
|
|
35
|
-
enabled: false
|
|
36
|
-
bot_token: ""
|
|
37
|
-
chat_id: ""
|
|
38
|
-
email:
|
|
39
|
-
enabled: false
|
|
40
|
-
to: ""
|
|
41
|
-
slack:
|
|
42
|
-
enabled: false
|
|
43
|
-
webhook_url: ""
|
package/docker-compose.yml
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# Glassmkr Crucible - docker compose deployment
|
|
2
|
-
#
|
|
3
|
-
# Before starting, create /etc/glassmkr/collector.yaml on the host with your
|
|
4
|
-
# Forge collector key. See https://forge.glassmkr.com/docs/getting-started.
|
|
5
|
-
#
|
|
6
|
-
# Why privileged + host network:
|
|
7
|
-
# - privileged: true gives access to /dev/ipmi0 (IPMI sensors) and raw disk devices (SMART)
|
|
8
|
-
# - network_mode: host lets the agent read the real host network interfaces and bond state
|
|
9
|
-
# - /proc and /sys are mounted so the agent monitors the host, not the container
|
|
10
|
-
|
|
11
|
-
services:
|
|
12
|
-
crucible:
|
|
13
|
-
image: ghcr.io/glassmkr/crucible:latest
|
|
14
|
-
container_name: glassmkr-crucible
|
|
15
|
-
restart: unless-stopped
|
|
16
|
-
privileged: true
|
|
17
|
-
network_mode: host
|
|
18
|
-
volumes:
|
|
19
|
-
- /etc/glassmkr:/etc/glassmkr:ro
|
|
20
|
-
- /proc:/host/proc:ro
|
|
21
|
-
- /sys:/host/sys:ro
|
|
22
|
-
- /dev:/dev:ro
|
|
23
|
-
- /run/dbus:/run/dbus:ro
|
|
24
|
-
environment:
|
|
25
|
-
- HOST_PROC=/host/proc
|
|
26
|
-
- HOST_SYS=/host/sys
|
package/scripts/sign-release.sh
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Sign a Crucible release
|
|
3
|
-
# Usage: ./scripts/sign-release.sh <version>
|
|
4
|
-
|
|
5
|
-
VERSION=$1
|
|
6
|
-
DIST_DIR="dist"
|
|
7
|
-
|
|
8
|
-
if [ -z "$VERSION" ]; then
|
|
9
|
-
echo "Usage: ./scripts/sign-release.sh <version>"
|
|
10
|
-
echo "Example: ./scripts/sign-release.sh v0.2.0"
|
|
11
|
-
exit 1
|
|
12
|
-
fi
|
|
13
|
-
|
|
14
|
-
echo "Signing Crucible $VERSION"
|
|
15
|
-
|
|
16
|
-
# Generate checksums
|
|
17
|
-
cd "$DIST_DIR" || exit 1
|
|
18
|
-
sha256sum *.tar.gz *.deb 2>/dev/null > SHA256SUMS || sha256sum *.js > SHA256SUMS
|
|
19
|
-
|
|
20
|
-
# Sign the checksums file
|
|
21
|
-
gpg --armor --detach-sign --local-user security@glassmkr.com SHA256SUMS
|
|
22
|
-
|
|
23
|
-
echo ""
|
|
24
|
-
echo "Release artifacts:"
|
|
25
|
-
ls -la SHA256SUMS SHA256SUMS.asc
|
|
26
|
-
echo ""
|
|
27
|
-
echo "Verify with:"
|
|
28
|
-
echo " gpg --verify SHA256SUMS.asc SHA256SUMS"
|
|
29
|
-
echo " sha256sum -c SHA256SUMS"
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { parseCliArgs, helpText, DEFAULT_CONFIG_PATH } from "../cli.js";
|
|
3
|
-
|
|
4
|
-
describe("parseCliArgs", () => {
|
|
5
|
-
it("--version returns version string and mode=version", () => {
|
|
6
|
-
const { result, output } = parseCliArgs(["--version"], "1.2.3");
|
|
7
|
-
expect(result.mode).toBe("version");
|
|
8
|
-
expect(output).toBe("glassmkr-crucible v1.2.3");
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
it("-v aliases --version", () => {
|
|
12
|
-
const { result, output } = parseCliArgs(["-v"], "1.2.3");
|
|
13
|
-
expect(result.mode).toBe("version");
|
|
14
|
-
expect(output).toBe("glassmkr-crucible v1.2.3");
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("--help returns help text and mode=help", () => {
|
|
18
|
-
const { result, output } = parseCliArgs(["--help"], "1.2.3");
|
|
19
|
-
expect(result.mode).toBe("help");
|
|
20
|
-
expect(output).toContain("glassmkr-crucible v1.2.3");
|
|
21
|
-
expect(output).toContain("Usage:");
|
|
22
|
-
expect(output).toContain("--version");
|
|
23
|
-
expect(output).toContain("--help");
|
|
24
|
-
expect(output).toContain("--config");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
it("-h aliases --help", () => {
|
|
28
|
-
const { result } = parseCliArgs(["-h"], "1.2.3");
|
|
29
|
-
expect(result.mode).toBe("help");
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("no args returns mode=run with the default config path", () => {
|
|
33
|
-
const { result, output } = parseCliArgs([], "1.2.3");
|
|
34
|
-
expect(result.mode).toBe("run");
|
|
35
|
-
expect(result.configPath).toBe(DEFAULT_CONFIG_PATH);
|
|
36
|
-
expect(output).toBeNull();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("-c accepts a path in the next argument", () => {
|
|
40
|
-
const { result } = parseCliArgs(["-c", "/tmp/a.yaml"], "1.2.3");
|
|
41
|
-
expect(result.configPath).toBe("/tmp/a.yaml");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("--config accepts a path in the next argument", () => {
|
|
45
|
-
const { result } = parseCliArgs(["--config", "/tmp/b.yaml"], "1.2.3");
|
|
46
|
-
expect(result.configPath).toBe("/tmp/b.yaml");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("--config=PATH form works", () => {
|
|
50
|
-
const { result } = parseCliArgs(["--config=/tmp/c.yaml"], "1.2.3");
|
|
51
|
-
expect(result.configPath).toBe("/tmp/c.yaml");
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("legacy positional argument still sets config path", () => {
|
|
55
|
-
const { result } = parseCliArgs(["/tmp/legacy.yaml"], "1.2.3");
|
|
56
|
-
expect(result.configPath).toBe("/tmp/legacy.yaml");
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it("--version wins over a provided config path (no collector start)", () => {
|
|
60
|
-
const { result } = parseCliArgs(["--config", "/tmp/x.yaml", "--version"], "1.2.3");
|
|
61
|
-
expect(result.mode).toBe("version");
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
describe("helpText", () => {
|
|
66
|
-
it("mentions the binary name, default config path, and both flags", () => {
|
|
67
|
-
const txt = helpText("0.6.1");
|
|
68
|
-
expect(txt).toContain("glassmkr-crucible v0.6.1");
|
|
69
|
-
expect(txt).toContain(DEFAULT_CONFIG_PATH);
|
|
70
|
-
expect(txt).toContain("-v, --version");
|
|
71
|
-
expect(txt).toContain("-h, --help");
|
|
72
|
-
expect(txt).toContain("-c, --config");
|
|
73
|
-
});
|
|
74
|
-
});
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { mkdtempSync, existsSync, writeFileSync, statSync, rmSync, chmodSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import {
|
|
6
|
-
consumeRebootMarker,
|
|
7
|
-
writeRebootMarker,
|
|
8
|
-
parseDuration,
|
|
9
|
-
} from "../lib/reboot-marker.js";
|
|
10
|
-
import { parseCliArgs } from "../cli.js";
|
|
11
|
-
|
|
12
|
-
let tmpDir: string;
|
|
13
|
-
let path: string;
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
tmpDir = mkdtempSync(join(tmpdir(), "crucible-test-"));
|
|
17
|
-
path = join(tmpDir, "reboot-expected");
|
|
18
|
-
});
|
|
19
|
-
afterEach(() => {
|
|
20
|
-
try { rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
describe("consumeRebootMarker", () => {
|
|
24
|
-
it("7. marker present, not expired: returns flag, deletes file", () => {
|
|
25
|
-
const future = new Date(Date.now() + 5 * 60_000).toISOString();
|
|
26
|
-
writeFileSync(path, JSON.stringify({ expires_at: future, reason: "kernel update" }));
|
|
27
|
-
const out = consumeRebootMarker(path);
|
|
28
|
-
expect(out).toEqual({ expected: true, reason: "kernel update" });
|
|
29
|
-
expect(existsSync(path)).toBe(false);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it("8. marker present, expired: returns null, deletes file", () => {
|
|
33
|
-
const past = new Date(Date.now() - 60_000).toISOString();
|
|
34
|
-
writeFileSync(path, JSON.stringify({ expires_at: past, reason: "stale" }));
|
|
35
|
-
expect(consumeRebootMarker(path)).toBeNull();
|
|
36
|
-
expect(existsSync(path)).toBe(false);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("9. marker absent: returns null, no throw", () => {
|
|
40
|
-
expect(consumeRebootMarker(path)).toBeNull();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("15. malformed JSON: returns null, file deleted, no crash", () => {
|
|
44
|
-
writeFileSync(path, "{not json at all");
|
|
45
|
-
expect(consumeRebootMarker(path)).toBeNull();
|
|
46
|
-
expect(existsSync(path)).toBe(false);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("invalid expires_at (missing): returns null, file deleted", () => {
|
|
50
|
-
writeFileSync(path, JSON.stringify({ reason: "oops" }));
|
|
51
|
-
expect(consumeRebootMarker(path)).toBeNull();
|
|
52
|
-
expect(existsSync(path)).toBe(false);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("consumed marker cannot be re-read (single-use)", () => {
|
|
56
|
-
const future = new Date(Date.now() + 60_000).toISOString();
|
|
57
|
-
writeFileSync(path, JSON.stringify({ expires_at: future }));
|
|
58
|
-
expect(consumeRebootMarker(path)).not.toBeNull();
|
|
59
|
-
expect(consumeRebootMarker(path)).toBeNull();
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe("writeRebootMarker", () => {
|
|
64
|
-
it("13. writes file at given path with correct TTL and reason, 0600 mode", () => {
|
|
65
|
-
const now = new Date("2026-04-21T22:00:00Z");
|
|
66
|
-
const res = writeRebootMarker({ path, reason: "kernel update", ttlMs: 10 * 60_000, now });
|
|
67
|
-
expect(res.path).toBe(path);
|
|
68
|
-
expect(res.expires_at).toBe("2026-04-21T22:10:00.000Z");
|
|
69
|
-
expect(existsSync(path)).toBe(true);
|
|
70
|
-
const mode = statSync(path).mode & 0o777;
|
|
71
|
-
expect(mode).toBe(0o600);
|
|
72
|
-
const round = consumeRebootMarker(path, new Date("2026-04-21T22:05:00Z"));
|
|
73
|
-
expect(round).toEqual({ expected: true, reason: "kernel update" });
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("default TTL is 10 minutes", () => {
|
|
77
|
-
const now = new Date("2026-04-21T22:00:00Z");
|
|
78
|
-
const res = writeRebootMarker({ path, now });
|
|
79
|
-
expect(res.expires_at).toBe("2026-04-21T22:10:00.000Z");
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
describe("parseDuration", () => {
|
|
84
|
-
it.each([
|
|
85
|
-
["10m", 600_000],
|
|
86
|
-
["2h", 7_200_000],
|
|
87
|
-
["600s", 600_000],
|
|
88
|
-
["500ms", 500],
|
|
89
|
-
["30", 30_000], // bare number -> seconds
|
|
90
|
-
])("%s -> %d ms", (input, ms) => {
|
|
91
|
-
expect(parseDuration(input)).toBe(ms);
|
|
92
|
-
});
|
|
93
|
-
it("rejects garbage", () => {
|
|
94
|
-
expect(parseDuration("forever")).toBeNull();
|
|
95
|
-
expect(parseDuration("-5m")).toBeNull();
|
|
96
|
-
expect(parseDuration("")).toBeNull();
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
describe("CLI parseCliArgs subcommands", () => {
|
|
101
|
-
it("14. `reboot` subcommand captured with flags", () => {
|
|
102
|
-
const { result } = parseCliArgs(["reboot", "--reason", "kernel update"], "1.0.0");
|
|
103
|
-
expect(result.mode).toBe("reboot");
|
|
104
|
-
expect(result.reason).toBe("kernel update");
|
|
105
|
-
});
|
|
106
|
-
it("`mark-reboot` with --ttl parsed through", () => {
|
|
107
|
-
const { result } = parseCliArgs(["mark-reboot", "--ttl=5m", "--reason=test"], "1.0.0");
|
|
108
|
-
expect(result.mode).toBe("mark-reboot");
|
|
109
|
-
expect(result.ttl).toBe("5m");
|
|
110
|
-
expect(result.reason).toBe("test");
|
|
111
|
-
});
|
|
112
|
-
it("`mark-reboot --help` returns help output without running", () => {
|
|
113
|
-
const { result, output } = parseCliArgs(["mark-reboot", "--help"], "1.0.0");
|
|
114
|
-
expect(result.mode).toBe("help");
|
|
115
|
-
expect(output).toContain("mark-reboot");
|
|
116
|
-
});
|
|
117
|
-
it("top-level help lists the new subcommands", () => {
|
|
118
|
-
const { output } = parseCliArgs(["--help"], "1.0.0");
|
|
119
|
-
expect(output).toMatch(/mark-reboot/);
|
|
120
|
-
expect(output).toMatch(/reboot/);
|
|
121
|
-
});
|
|
122
|
-
});
|
package/src/alerts/evaluator.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { allRules } from "./rules.js";
|
|
2
|
-
import type { Snapshot, AlertResult } from "../lib/types.js";
|
|
3
|
-
import type { Config } from "../config.js";
|
|
4
|
-
|
|
5
|
-
export function evaluateAlerts(snapshot: Snapshot, thresholds: Config["thresholds"]): AlertResult[] {
|
|
6
|
-
const results: AlertResult[] = [];
|
|
7
|
-
for (const rule of allRules) {
|
|
8
|
-
try {
|
|
9
|
-
results.push(...rule.evaluate(snapshot, thresholds));
|
|
10
|
-
} catch (err) {
|
|
11
|
-
console.error(`[alerts] Rule ${rule.type} error:`, err);
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
return results;
|
|
15
|
-
}
|