fix: resolve 13 release-consistency findings
Documentation: - Fix CI badge href to /actions (was 404 on Gitea) - Add UPTOP_METRICS_PUBLIC + UPTOP_MAINT_RETENTION to README env table - Link maintenance retention to env var name in data retention section - Note metrics auth requirement in features list - Fix clustering.md: fail-closed wording, mark AGG_STRATEGY/NODE_REGION optional - Fix .env.example: wording (no .env loader), add TRUSTED_PROXIES + MAINT_RETENTION - Add CLI help/usage with subcommand listing, accept serve/help/-h/-version Docker/deploy: - Add EXPOSE 8080 to Dockerfile - Remove dead LIPGLOSS_RENDERER_HAS_DARK_BACKGROUND env - Exempt /api/health from cluster auth (fixes Docker HEALTHCHECK 401) - Add sysctls for unprivileged ping to all compose files Cosmetic: - Fix bug_report.yaml: SemVer placeholder, remove nonexistent serve subcommand
This commit is contained in:
+3
-1
@@ -1,5 +1,5 @@
|
|||||||
# ─── uptop configuration ───────────────────────────────────
|
# ─── uptop configuration ───────────────────────────────────
|
||||||
# Copy to .env and edit. Only uncomment what you need.
|
# Export in your environment or pass via docker run --env-file.\n# Only uncomment what you need.
|
||||||
|
|
||||||
# ─── Core ──────────────────────────────────────────────────
|
# ─── Core ──────────────────────────────────────────────────
|
||||||
UPTOP_PORT=23234 # SSH server port
|
UPTOP_PORT=23234 # SSH server port
|
||||||
@@ -40,3 +40,5 @@ UPTOP_DB_DSN=/data/uptop.db # File path (SQLite) or connection string (Postgre
|
|||||||
# UPTOP_ALLOW_PRIVATE_TARGETS=false # Allow monitoring RFC1918/loopback addresses
|
# UPTOP_ALLOW_PRIVATE_TARGETS=false # Allow monitoring RFC1918/loopback addresses
|
||||||
# UPTOP_METRICS_PUBLIC=false # Expose /metrics without auth
|
# UPTOP_METRICS_PUBLIC=false # Expose /metrics without auth
|
||||||
# UPTOP_CORS_ORIGIN= # Access-Control-Allow-Origin for /status/json
|
# UPTOP_CORS_ORIGIN= # Access-Control-Allow-Origin for /status/json
|
||||||
|
# UPTOP_TRUSTED_PROXIES= # Comma-separated CIDRs/IPs for X-Forwarded-For trust
|
||||||
|
# UPTOP_MAINT_RETENTION=168h # How long ended maintenance windows are kept
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ body:
|
|||||||
label: What happened?
|
label: What happened?
|
||||||
description: Include what you expected to happen instead.
|
description: Include what you expected to happen instead.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
When I run `uptop serve`, the TUI crashes after 10 seconds.
|
When I run `uptop`, the TUI crashes after 10 seconds.
|
||||||
I expected it to keep running and display monitor status.
|
I expected it to keep running and display monitor status.
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@@ -25,7 +25,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Steps to reproduce
|
label: Steps to reproduce
|
||||||
placeholder: |
|
placeholder: |
|
||||||
1. Run `uptop serve`
|
1. Run `uptop`
|
||||||
2. Wait ~10 seconds
|
2. Wait ~10 seconds
|
||||||
3. TUI crashes with panic
|
3. TUI crashes with panic
|
||||||
validations:
|
validations:
|
||||||
@@ -37,7 +37,7 @@ body:
|
|||||||
description: Output of `uptop version`, OS, terminal. Paste any errors below.
|
description: Output of `uptop version`, OS, terminal. Paste any errors below.
|
||||||
render: shell
|
render: shell
|
||||||
placeholder: |
|
placeholder: |
|
||||||
uptop version 2026.06.1
|
uptop 0.1.0 (abc1234, 2026-06-17)
|
||||||
OS: Debian 13
|
OS: Debian 13
|
||||||
Terminal: Ghostty
|
Terminal: Ghostty
|
||||||
|
|
||||||
|
|||||||
+1
-2
@@ -23,14 +23,13 @@ RUN mkdir -p /data/.ssh && chown -R uptop:uptop /data
|
|||||||
COPY --from=builder /app/uptop .
|
COPY --from=builder /app/uptop .
|
||||||
COPY --chmod=755 docker-entrypoint.sh /usr/local/bin/
|
COPY --chmod=755 docker-entrypoint.sh /usr/local/bin/
|
||||||
|
|
||||||
ENV LIPGLOSS_RENDERER_HAS_DARK_BACKGROUND=true
|
|
||||||
ENV UPTOP_DB_TYPE=sqlite
|
ENV UPTOP_DB_TYPE=sqlite
|
||||||
ENV UPTOP_DB_DSN=/data/uptop.db
|
ENV UPTOP_DB_DSN=/data/uptop.db
|
||||||
ENV UPTOP_KEYS=/data/authorized_keys
|
ENV UPTOP_KEYS=/data/authorized_keys
|
||||||
ENV UPTOP_SSH_HOST_KEY=/data/.ssh/id_ed25519
|
ENV UPTOP_SSH_HOST_KEY=/data/.ssh/id_ed25519
|
||||||
ENV UPTOP_PORT=23234
|
ENV UPTOP_PORT=23234
|
||||||
|
|
||||||
EXPOSE 23234
|
EXPOSE 8080 23234
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
CMD wget -qO- http://localhost:8080/api/health || exit 1
|
CMD wget -qO- http://localhost:8080/api/health || exit 1
|
||||||
USER uptop
|
USER uptop
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<p>No browser. No client install. Just <code>ssh -p 23234 your-server</code>.</p>
|
<p>No browser. No client install. Just <code>ssh -p 23234 your-server</code>.</p>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a href="https://gitea.lerkolabs.com/lerkolabs/uptop/actions/workflows/ci.yml"><img src="https://gitea.lerkolabs.com/lerkolabs/uptop/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
<a href="https://gitea.lerkolabs.com/lerkolabs/uptop/actions"><img src="https://gitea.lerkolabs.com/lerkolabs/uptop/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
|
||||||
<img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT License">
|
<img src="https://img.shields.io/badge/license-MIT-blue" alt="MIT License">
|
||||||
<img src="https://img.shields.io/badge/go-1.26-00ADD8?logo=go&logoColor=white" alt="Go 1.26">
|
<img src="https://img.shields.io/badge/go-1.26-00ADD8?logo=go&logoColor=white" alt="Go 1.26">
|
||||||
<img src="https://img.shields.io/docker/pulls/lerkolabs/uptop" alt="Docker Pulls">
|
<img src="https://img.shields.io/docker/pulls/lerkolabs/uptop" alt="Docker Pulls">
|
||||||
@@ -27,7 +27,7 @@ Canonical repo: [gitea.lerkolabs.com/lerkolabs/uptop](https://gitea.lerkolabs.co
|
|||||||
- **10 alert providers** — Discord, Slack, Email, Ntfy, Webhook, Telegram, PagerDuty, Pushover, Gotify, Opsgenie
|
- **10 alert providers** — Discord, Slack, Email, Ntfy, Webhook, Telegram, PagerDuty, Pushover, Gotify, Opsgenie
|
||||||
- **Config as code** — define monitors in YAML, apply declaratively, version control your setup
|
- **Config as code** — define monitors in YAML, apply declaratively, version control your setup
|
||||||
- **HA clustering** — leader/follower with automatic failover
|
- **HA clustering** — leader/follower with automatic failover
|
||||||
- **Prometheus metrics** — `/metrics` endpoint, wire it straight to Grafana
|
- **Prometheus metrics** — `/metrics` endpoint (`UPTOP_METRICS_PUBLIC=true` to expose without auth)
|
||||||
- **Public status page** — HTML + JSON, toggle with an env var
|
- **Public status page** — HTML + JSON, toggle with an env var
|
||||||
- **SQLite or Postgres** — SQLite for single-node, Postgres for production
|
- **SQLite or Postgres** — SQLite for single-node, Postgres for production
|
||||||
- **Uptime Kuma import** — migrate from Kuma with one command
|
- **Uptime Kuma import** — migrate from Kuma with one command
|
||||||
@@ -146,6 +146,8 @@ Full reference in [docs/config-as-code.md](docs/config-as-code.md).
|
|||||||
| `UPTOP_INSECURE_SKIP_VERIFY` | `false` | Skip TLS verification for checks |
|
| `UPTOP_INSECURE_SKIP_VERIFY` | `false` | Skip TLS verification for checks |
|
||||||
| `UPTOP_ALLOW_PRIVATE_TARGETS` | `false` | Allow monitoring RFC1918/loopback addresses |
|
| `UPTOP_ALLOW_PRIVATE_TARGETS` | `false` | Allow monitoring RFC1918/loopback addresses |
|
||||||
| `UPTOP_ADMIN_KEY` | | SSH public key seeded as first admin on startup |
|
| `UPTOP_ADMIN_KEY` | | SSH public key seeded as first admin on startup |
|
||||||
|
| `UPTOP_METRICS_PUBLIC` | `false` | Expose `/metrics` without auth |
|
||||||
|
| `UPTOP_MAINT_RETENTION` | `168h` | How long ended maintenance windows are kept |
|
||||||
| `UPTOP_TRUSTED_PROXIES` | | Comma-separated CIDRs/IPs whose `X-Forwarded-For` is trusted ([details](#running-behind-a-reverse-proxy)) |
|
| `UPTOP_TRUSTED_PROXIES` | | Comma-separated CIDRs/IPs whose `X-Forwarded-For` is trusted ([details](#running-behind-a-reverse-proxy)) |
|
||||||
|
|
||||||
See [`.env.example`](.env.example) for all options including TLS, probes, and advanced settings.
|
See [`.env.example`](.env.example) for all options including TLS, probes, and advanced settings.
|
||||||
@@ -179,7 +181,7 @@ uptop prunes its own history in the background — no external cleanup jobs need
|
|||||||
| Check history | newest 1,000 checks per monitor |
|
| Check history | newest 1,000 checks per monitor |
|
||||||
| State changes (UP/DOWN transitions) | newest 5,000 per monitor |
|
| State changes (UP/DOWN transitions) | newest 5,000 per monitor |
|
||||||
| Logs | newest 200 entries |
|
| Logs | newest 200 entries |
|
||||||
| Maintenance windows | 7 days after they end (configurable) |
|
| Maintenance windows | 7 days after they end (`UPTOP_MAINT_RETENTION`) |
|
||||||
|
|
||||||
Sparklines, uptime percentages, and SLA reports are computed from these windows, so very long-horizon stats aren't retained. Export to Prometheus via `/metrics` if you need unlimited history.
|
Sparklines, uptime percentages, and SLA reports are computed from these windows, so very long-horizon stats aren't retained. Export to Prometheus via `/metrics` if you need unlimited history.
|
||||||
|
|
||||||
|
|||||||
+21
-1
@@ -77,17 +77,37 @@ func main() {
|
|||||||
case "export":
|
case "export":
|
||||||
runExport(os.Args[2:])
|
runExport(os.Args[2:])
|
||||||
return
|
return
|
||||||
case "version", "--version", "-v":
|
case "version", "--version", "-v", "-version":
|
||||||
printVersion()
|
printVersion()
|
||||||
return
|
return
|
||||||
case "migrate-secrets":
|
case "migrate-secrets":
|
||||||
runMigrateSecrets(os.Args[2:])
|
runMigrateSecrets(os.Args[2:])
|
||||||
return
|
return
|
||||||
|
case "help", "--help", "-h":
|
||||||
|
printUsage()
|
||||||
|
return
|
||||||
|
case "serve":
|
||||||
|
runServe(os.Args[2:])
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
runServe(os.Args[1:])
|
runServe(os.Args[1:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func printUsage() {
|
||||||
|
fmt.Fprintf(os.Stderr, `Usage: uptop <command> [flags]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
serve Start the server (default if no command given)
|
||||||
|
apply Apply monitors from a YAML file
|
||||||
|
export Export monitors to YAML
|
||||||
|
migrate-secrets Re-encrypt alert credentials with current key
|
||||||
|
version Print version and exit
|
||||||
|
|
||||||
|
Run 'uptop serve --help' for server flags.
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
func printVersion() {
|
func printVersion() {
|
||||||
out := "uptop " + version
|
out := "uptop " + version
|
||||||
var meta []string
|
var meta []string
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ services:
|
|||||||
leader:
|
leader:
|
||||||
image: lerkolabs/uptop:latest
|
image: lerkolabs/uptop:latest
|
||||||
container_name: uptop-leader
|
container_name: uptop-leader
|
||||||
|
sysctls:
|
||||||
|
- net.ipv4.ping_group_range=0 2147483647
|
||||||
ports:
|
ports:
|
||||||
- "23234:23234" # SSH
|
- "23234:23234" # SSH
|
||||||
- "8080:8080" # HTTP
|
- "8080:8080" # HTTP
|
||||||
@@ -40,6 +42,8 @@ services:
|
|||||||
follower:
|
follower:
|
||||||
image: lerkolabs/uptop:latest
|
image: lerkolabs/uptop:latest
|
||||||
container_name: uptop-follower
|
container_name: uptop-follower
|
||||||
|
sysctls:
|
||||||
|
- net.ipv4.ping_group_range=0 2147483647
|
||||||
ports:
|
ports:
|
||||||
- "23233:23234" # SSH (Mapped to different host port)
|
- "23233:23234" # SSH (Mapped to different host port)
|
||||||
- "8081:8080" # HTTP (Mapped to different host port)
|
- "8081:8080" # HTTP (Mapped to different host port)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
services:
|
services:
|
||||||
leader:
|
leader:
|
||||||
image: lerkolabs/uptop:latest
|
image: lerkolabs/uptop:latest
|
||||||
|
sysctls:
|
||||||
|
- net.ipv4.ping_group_range=0 2147483647
|
||||||
environment:
|
environment:
|
||||||
- UPTOP_CLUSTER_MODE=leader
|
- UPTOP_CLUSTER_MODE=leader
|
||||||
- UPTOP_CLUSTER_SECRET=changeme # EXAMPLE ONLY — rotate before use
|
- UPTOP_CLUSTER_SECRET=changeme # EXAMPLE ONLY — rotate before use
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ services:
|
|||||||
- ALL
|
- ALL
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
|
sysctls:
|
||||||
|
- net.ipv4.ping_group_range=0 2147483647
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- /tmp
|
- /tmp
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
+2
-4
@@ -47,13 +47,11 @@ Probes are lightweight, stateless nodes that run checks from different locations
|
|||||||
| Node | Variable | Value |
|
| Node | Variable | Value |
|
||||||
|------|----------|-------|
|
|------|----------|-------|
|
||||||
| Both | `UPTOP_CLUSTER_SECRET` | Same shared secret |
|
| Both | `UPTOP_CLUSTER_SECRET` | Same shared secret |
|
||||||
| Leader | `UPTOP_AGG_STRATEGY` | `any-down`, `majority-down`, or `all-down` |
|
|
||||||
| Probe | `UPTOP_CLUSTER_MODE` | `probe` |
|
| Probe | `UPTOP_CLUSTER_MODE` | `probe` |
|
||||||
| Probe | `UPTOP_PEER_URL` | Leader's HTTP URL |
|
| Probe | `UPTOP_PEER_URL` | Leader's HTTP URL |
|
||||||
| Probe | `UPTOP_NODE_ID` | Unique identifier (e.g. `probe-us-east`) |
|
| Probe | `UPTOP_NODE_ID` | Unique identifier (e.g. `probe-us-east`) |
|
||||||
| Probe | `UPTOP_NODE_REGION` | Region tag matching monitor assignments |
|
|
||||||
|
|
||||||
Optional: `UPTOP_NODE_NAME` for a human-readable label in the TUI.
|
Optional: `UPTOP_AGG_STRATEGY` (default `any-down`), `UPTOP_NODE_REGION` (omit to match all monitors), `UPTOP_NODE_NAME` (human-readable label in the TUI).
|
||||||
|
|
||||||
See [`deploy/docker-compose.probe.yml`](../deploy/docker-compose.probe.yml) for a multi-region example.
|
See [`deploy/docker-compose.probe.yml`](../deploy/docker-compose.probe.yml) for a multi-region example.
|
||||||
|
|
||||||
@@ -80,6 +78,6 @@ Set via `UPTOP_AGG_STRATEGY` on the leader.
|
|||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
- Set `UPTOP_CLUSTER_SECRET` on all nodes. Without it, cluster API endpoints are unauthenticated.
|
- Set `UPTOP_CLUSTER_SECRET` on all nodes. Without it, cluster API endpoints reject all requests (fail closed); only `/api/health` stays open.
|
||||||
- Secrets are sent in HTTP headers (`X-Uptop-Secret`). Use TLS or a reverse proxy for production.
|
- Secrets are sent in HTTP headers (`X-Uptop-Secret`). Use TLS or a reverse proxy for production.
|
||||||
- uptop warns on startup if the cluster secret is missing or if cluster mode is active without TLS.
|
- uptop warns on startup if the cluster secret is missing or if cluster mode is active without TLS.
|
||||||
|
|||||||
@@ -159,10 +159,6 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.cfg.ClusterKey != "" && !checkSecret(r.Header.Get("X-Uptop-Secret"), s.cfg.ClusterKey) {
|
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
_, _ = w.Write([]byte("OK"))
|
_, _ = w.Write([]byte("OK"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,8 +219,8 @@ func TestHealth_WrongSecret(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode != 401 {
|
if resp.StatusCode != 200 {
|
||||||
t.Errorf("expected 401, got %d", resp.StatusCode)
|
t.Errorf("health is unauthenticated, expected 200, got %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user