From b32145fb5864a001e7a354c090f825a9234f6c4e Mon Sep 17 00:00:00 2001 From: Tyler Koenig Date: Fri, 19 Jun 2026 20:09:03 -0400 Subject: [PATCH] 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 --- .env.example | 4 +++- .gitea/issue_template/bug_report.yaml | 6 +++--- Dockerfile | 3 +-- README.md | 8 +++++--- cmd/uptop/main.go | 22 +++++++++++++++++++++- deploy/docker-compose.cluster.yml | 4 ++++ deploy/docker-compose.probe.yml | 2 ++ deploy/docker-compose.yml | 2 ++ docs/clustering.md | 6 ++---- internal/server/server.go | 4 ---- internal/server/server_test.go | 4 ++-- 11 files changed, 45 insertions(+), 20 deletions(-) diff --git a/.env.example b/.env.example index 29dd395..a24af3a 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ # ─── 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 ────────────────────────────────────────────────── 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_METRICS_PUBLIC=false # Expose /metrics without auth # 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 diff --git a/.gitea/issue_template/bug_report.yaml b/.gitea/issue_template/bug_report.yaml index 1aff83a..fcc82ad 100644 --- a/.gitea/issue_template/bug_report.yaml +++ b/.gitea/issue_template/bug_report.yaml @@ -16,7 +16,7 @@ body: label: What happened? description: Include what you expected to happen instead. 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. validations: required: true @@ -25,7 +25,7 @@ body: attributes: label: Steps to reproduce placeholder: | - 1. Run `uptop serve` + 1. Run `uptop` 2. Wait ~10 seconds 3. TUI crashes with panic validations: @@ -37,7 +37,7 @@ body: description: Output of `uptop version`, OS, terminal. Paste any errors below. render: shell placeholder: | - uptop version 2026.06.1 + uptop 0.1.0 (abc1234, 2026-06-17) OS: Debian 13 Terminal: Ghostty diff --git a/Dockerfile b/Dockerfile index aada010..40fdb00 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,14 +23,13 @@ RUN mkdir -p /data/.ssh && chown -R uptop:uptop /data COPY --from=builder /app/uptop . COPY --chmod=755 docker-entrypoint.sh /usr/local/bin/ -ENV LIPGLOSS_RENDERER_HAS_DARK_BACKGROUND=true ENV UPTOP_DB_TYPE=sqlite ENV UPTOP_DB_DSN=/data/uptop.db ENV UPTOP_KEYS=/data/authorized_keys ENV UPTOP_SSH_HOST_KEY=/data/.ssh/id_ed25519 ENV UPTOP_PORT=23234 -EXPOSE 23234 +EXPOSE 8080 23234 HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD wget -qO- http://localhost:8080/api/health || exit 1 USER uptop diff --git a/README.md b/README.md index aaadcc4..699c69b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@

No browser. No client install. Just ssh -p 23234 your-server.

- CI + CI MIT License Go 1.26 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 - **Config as code** — define monitors in YAML, apply declaratively, version control your setup - **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 - **SQLite or Postgres** — SQLite for single-node, Postgres for production - **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_ALLOW_PRIVATE_TARGETS` | `false` | Allow monitoring RFC1918/loopback addresses | | `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)) | 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 | | State changes (UP/DOWN transitions) | newest 5,000 per monitor | | 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. diff --git a/cmd/uptop/main.go b/cmd/uptop/main.go index fcaef41..9449382 100644 --- a/cmd/uptop/main.go +++ b/cmd/uptop/main.go @@ -77,17 +77,37 @@ func main() { case "export": runExport(os.Args[2:]) return - case "version", "--version", "-v": + case "version", "--version", "-v", "-version": printVersion() return case "migrate-secrets": runMigrateSecrets(os.Args[2:]) return + case "help", "--help", "-h": + printUsage() + return + case "serve": + runServe(os.Args[2:]) + return } } runServe(os.Args[1:]) } +func printUsage() { + fmt.Fprintf(os.Stderr, `Usage: uptop [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() { out := "uptop " + version var meta []string diff --git a/deploy/docker-compose.cluster.yml b/deploy/docker-compose.cluster.yml index 64ea27c..d33bc71 100644 --- a/deploy/docker-compose.cluster.yml +++ b/deploy/docker-compose.cluster.yml @@ -5,6 +5,8 @@ services: leader: image: lerkolabs/uptop:latest container_name: uptop-leader + sysctls: + - net.ipv4.ping_group_range=0 2147483647 ports: - "23234:23234" # SSH - "8080:8080" # HTTP @@ -40,6 +42,8 @@ services: follower: image: lerkolabs/uptop:latest container_name: uptop-follower + sysctls: + - net.ipv4.ping_group_range=0 2147483647 ports: - "23233:23234" # SSH (Mapped to different host port) - "8081:8080" # HTTP (Mapped to different host port) diff --git a/deploy/docker-compose.probe.yml b/deploy/docker-compose.probe.yml index 28e4ecd..d0d5bc6 100644 --- a/deploy/docker-compose.probe.yml +++ b/deploy/docker-compose.probe.yml @@ -1,6 +1,8 @@ services: leader: image: lerkolabs/uptop:latest + sysctls: + - net.ipv4.ping_group_range=0 2147483647 environment: - UPTOP_CLUSTER_MODE=leader - UPTOP_CLUSTER_SECRET=changeme # EXAMPLE ONLY — rotate before use diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 8131c93..c00cc87 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -8,6 +8,8 @@ services: - ALL security_opt: - no-new-privileges:true + sysctls: + - net.ipv4.ping_group_range=0 2147483647 tmpfs: - /tmp ports: diff --git a/docs/clustering.md b/docs/clustering.md index 85b5f63..f05869e 100644 --- a/docs/clustering.md +++ b/docs/clustering.md @@ -47,13 +47,11 @@ Probes are lightweight, stateless nodes that run checks from different locations | Node | Variable | Value | |------|----------|-------| | 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_PEER_URL` | Leader's HTTP URL | | 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. @@ -80,6 +78,6 @@ Set via `UPTOP_AGG_STRATEGY` on the leader. ## 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. - uptop warns on startup if the cluster secret is missing or if cluster mode is active without TLS. diff --git a/internal/server/server.go b/internal/server/server.go index 43026c8..6f349c8 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -159,10 +159,6 @@ func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 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.Write([]byte("OK")) } diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 27cbda3..d92ec4b 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -219,8 +219,8 @@ func TestHealth_WrongSecret(t *testing.T) { t.Fatal(err) } defer resp.Body.Close() - if resp.StatusCode != 401 { - t.Errorf("expected 401, got %d", resp.StatusCode) + if resp.StatusCode != 200 { + t.Errorf("health is unauthenticated, expected 200, got %d", resp.StatusCode) } }