From 367e9f3941763006df2e223deddd621a81934235 Mon Sep 17 00:00:00 2001 From: pgray Date: Wed, 21 Jan 2026 18:33:29 +0000 Subject: [PATCH 1/4] install-scripts --- CLAUDE.md | 1 + deploy/README.md | 71 +++++++++++++++++++++++++ deploy/ndld-update.service | 20 +++++++ deploy/ndld-update.timer | 10 ++++ deploy/setup.sh | 103 +++++++++++++++++++++++++++++++++++++ deploy/update.sh | 68 ++++++++++++++++++++++++ 6 files changed, 273 insertions(+) create mode 100644 deploy/README.md create mode 100644 deploy/ndld-update.service create mode 100644 deploy/ndld-update.timer create mode 100755 deploy/setup.sh create mode 100755 deploy/update.sh diff --git a/CLAUDE.md b/CLAUDE.md index f7fba5e..52927bc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -172,6 +172,7 @@ cargo test --workspace # Run all tests (optional but recommende **Required**: `cargo fmt`, `cargo clippy`, and `cargo check` must pass without errors. **Recommended**: Fix all clippy warnings before committing. Run full build and tests when making significant changes. +**IMPORTANT**: UPDATE ANY README FILES RELEVANT TO CHANGES ## Updating Dependencies diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..9618127 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,71 @@ +# ndld deployment + +Auto-update script and systemd units for ndld. + +## Install + +```bash +# Clone or copy deploy/ to server, then: +sudo ./setup.sh install + +# Or specify custom install dir: +sudo INSTALL_DIR=/srv/ndld ./setup.sh install +``` + +## Uninstall + +```bash +sudo ./setup.sh uninstall +``` + +## Check for changes + +```bash +./setup.sh diff # exits 0 if up to date, 1 if changes detected +``` + +## Configuration + +Edit `/opt/ndld/ndld-update.service` for environment: + +| Variable | Default | Description | +|----------|---------|-------------| +| `COMPOSE_DIR` | `/opt/ndld` | Path to docker-compose.yml | +| `HEALTH_URL` | `http://localhost:8080/health` | Health check endpoint | +| `HEALTH_TIMEOUT` | `30` | Seconds to wait for health check | + +Edit `/opt/ndld/ndld-update.timer` for schedule: + +| Setting | Default | Description | +|---------|---------|-------------| +| `OnBootSec` | `5min` | First run after boot | +| `OnUnitActiveSec` | `15min` | Interval between runs | +| `RandomizedDelaySec` | `2min` | Random jitter | + +After changes: `sudo systemctl daemon-reload` + +## Commands + +```bash +# Check timer status +systemctl status ndld-update.timer + +# View logs +journalctl -u ndld-update.service -f + +# Run manually +sudo systemctl start ndld-update.service + +# List recent runs +systemctl list-timers ndld-update.timer +``` + +## Security + +The setup applies these protections: + +- **File permissions**: Scripts owned by root, not world-writable +- **systemd hardening**: `ProtectSystem=strict`, `ProtectHome=yes`, `PrivateTmp=yes`, `NoNewPrivileges=yes` +- **Path validation**: Requires absolute paths, checks compose file exists +- **Health checks**: Verifies service is healthy after restart +- **Rollback**: Attempts to restore previous state if restart fails diff --git a/deploy/ndld-update.service b/deploy/ndld-update.service new file mode 100644 index 0000000..dacda53 --- /dev/null +++ b/deploy/ndld-update.service @@ -0,0 +1,20 @@ +[Unit] +Description=Update ndld Docker container +After=docker.service +Requires=docker.service + +[Service] +Type=oneshot +ExecStart=/opt/ndld/update.sh +Environment=COMPOSE_DIR=/opt/ndld +Environment=HEALTH_URL=http://localhost:8080/health + +# Security hardening +ProtectSystem=strict +ProtectHome=yes +PrivateTmp=yes +NoNewPrivileges=yes +ReadWritePaths=/opt/ndld + +[Install] +WantedBy=multi-user.target diff --git a/deploy/ndld-update.timer b/deploy/ndld-update.timer new file mode 100644 index 0000000..8556993 --- /dev/null +++ b/deploy/ndld-update.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Check for ndld updates + +[Timer] +OnBootSec=5min +OnUnitActiveSec=15min +RandomizedDelaySec=2min + +[Install] +WantedBy=timers.target diff --git a/deploy/setup.sh b/deploy/setup.sh new file mode 100755 index 0000000..aee4513 --- /dev/null +++ b/deploy/setup.sh @@ -0,0 +1,103 @@ +#!/bin/sh +set -eu + +INSTALL_DIR="${INSTALL_DIR:-/opt/ndld}" +SYSTEMD_DIR="/etc/systemd/system" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +usage() { + echo "Usage: $0 [install|uninstall|diff]" + exit 1 +} + +require_root() { + if [ "$(id -u)" -ne 0 ]; then + echo "This script must be run as root (or with sudo)" + exit 1 + fi +} + +install() { + require_root + echo "Installing ndld auto-update to $INSTALL_DIR..." + + # Validate source files exist + for f in update.sh ndld-update.service ndld-update.timer; do + [ -f "$SCRIPT_DIR/$f" ] || { echo "Missing: $SCRIPT_DIR/$f"; exit 1; } + done + + # Create install dir (root-owned, no world-write) + mkdir -p "$INSTALL_DIR" + chmod 755 "$INSTALL_DIR" + + # Copy files (root:root, not world-writable) + cp "$SCRIPT_DIR/update.sh" "$INSTALL_DIR/" + cp "$SCRIPT_DIR/ndld-update.service" "$INSTALL_DIR/" + cp "$SCRIPT_DIR/ndld-update.timer" "$INSTALL_DIR/" + + chown root:root "$INSTALL_DIR/update.sh" + chown root:root "$INSTALL_DIR/ndld-update.service" + chown root:root "$INSTALL_DIR/ndld-update.timer" + chmod 755 "$INSTALL_DIR/update.sh" + chmod 644 "$INSTALL_DIR/ndld-update.service" + chmod 644 "$INSTALL_DIR/ndld-update.timer" + + # Symlink systemd units + ln -sf "$INSTALL_DIR/ndld-update.service" "$SYSTEMD_DIR/" + ln -sf "$INSTALL_DIR/ndld-update.timer" "$SYSTEMD_DIR/" + + # Enable and start timer + systemctl daemon-reload + systemctl enable --now ndld-update.timer + + echo "Done. Check status with: systemctl status ndld-update.timer" +} + +uninstall() { + require_root + echo "Uninstalling ndld auto-update..." + + # Stop and disable timer + systemctl disable --now ndld-update.timer 2>/dev/null || true + systemctl stop ndld-update.service 2>/dev/null || true + + # Remove symlinks + rm -f "$SYSTEMD_DIR/ndld-update.service" + rm -f "$SYSTEMD_DIR/ndld-update.timer" + + # Remove installed files (but not compose files) + rm -f "$INSTALL_DIR/update.sh" + rm -f "$INSTALL_DIR/ndld-update.service" + rm -f "$INSTALL_DIR/ndld-update.timer" + + systemctl daemon-reload + + echo "Done. Auto-update disabled." +} + +diff_files() { + echo "Comparing $SCRIPT_DIR -> $INSTALL_DIR" + echo "" + changed=0 + for f in update.sh ndld-update.service ndld-update.timer; do + if [ ! -f "$INSTALL_DIR/$f" ]; then + echo "$f: not installed" + changed=1 + elif ! diff -q "$SCRIPT_DIR/$f" "$INSTALL_DIR/$f" >/dev/null 2>&1; then + echo "$f: differs" + diff --color=auto -u "$INSTALL_DIR/$f" "$SCRIPT_DIR/$f" || true + echo "" + changed=1 + else + echo "$f: up to date" + fi + done + exit $changed +} + +case "${1:-}" in + install) install ;; + uninstall) uninstall ;; + diff) diff_files ;; + *) usage ;; +esac diff --git a/deploy/update.sh b/deploy/update.sh new file mode 100755 index 0000000..b6fbc09 --- /dev/null +++ b/deploy/update.sh @@ -0,0 +1,68 @@ +#!/bin/sh +set -eu + +# Configuration +COMPOSE_DIR="${COMPOSE_DIR:-/opt/ndld}" +HEALTH_URL="${HEALTH_URL:-http://localhost:8080/health}" +HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-30}" +LOG_PREFIX="[ndld-update]" + +log() { + echo "$LOG_PREFIX $(date '+%Y-%m-%d %H:%M:%S') $1" +} + +die() { + log "ERROR: $1" + exit 1 +} + +# Validate COMPOSE_DIR +case "$COMPOSE_DIR" in + /*) ;; # absolute path, ok + *) die "COMPOSE_DIR must be an absolute path: $COMPOSE_DIR" ;; +esac + +[ -d "$COMPOSE_DIR" ] || die "COMPOSE_DIR does not exist: $COMPOSE_DIR" +[ -f "$COMPOSE_DIR/docker-compose.yml" ] || [ -f "$COMPOSE_DIR/compose.yml" ] || \ + die "No compose file found in $COMPOSE_DIR" + +cd "$COMPOSE_DIR" + +# Capture current image IDs for rollback +PREV_IMAGES=$(docker compose images -q 2>/dev/null || true) + +log "Pulling latest images..." +if ! docker compose pull; then + die "Pull failed" +fi + +# Check if images actually changed +NEW_IMAGES=$(docker compose images -q 2>/dev/null || true) +if [ "$PREV_IMAGES" = "$NEW_IMAGES" ]; then + log "No new images, skipping restart" + exit 0 +fi + +log "New images detected, restarting services..." +if ! docker compose up -d; then + log "Restart failed, attempting rollback..." + docker compose down 2>/dev/null || true + docker compose up -d 2>/dev/null || die "Rollback failed" + die "Restart failed (rollback attempted)" +fi + +# Health check +log "Waiting for health check..." +elapsed=0 +while [ $elapsed -lt "$HEALTH_TIMEOUT" ]; do + if curl -sf "$HEALTH_URL" >/dev/null 2>&1; then + log "Health check passed" + log "Done" + exit 0 + fi + sleep 2 + elapsed=$((elapsed + 2)) +done + +log "WARNING: Health check failed after ${HEALTH_TIMEOUT}s (service may still be starting)" +exit 0 From 068bf7f312d3ed899b716038480cd1e8ce46b896 Mon Sep 17 00:00:00 2001 From: pgray Date: Wed, 21 Jan 2026 19:00:01 +0000 Subject: [PATCH 2/4] compose --- deploy/README.md | 13 +++++++++++++ deploy/setup.sh | 34 ++++++++++++++++++++++++++++++++-- deploy/update.sh | 23 +++++++++-------------- 3 files changed, 54 insertions(+), 16 deletions(-) diff --git a/deploy/README.md b/deploy/README.md index 9618127..1b82dae 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -12,6 +12,19 @@ sudo ./setup.sh install sudo INSTALL_DIR=/srv/ndld ./setup.sh install ``` +Create `.env` with required secrets: + +```bash +cat > /opt/ndld/.env << 'EOF' +NDL_CLIENT_ID=your_client_id +NDL_CLIENT_SECRET=your_client_secret +NDLD_PUBLIC_URL=https://ndl.example.com +NDLD_ACME_DOMAIN=ndl.example.com +NDLD_ACME_EMAIL=you@example.com +EOF +chmod 600 /opt/ndld/.env +``` + ## Uninstall ```bash diff --git a/deploy/setup.sh b/deploy/setup.sh index aee4513..52e8bd3 100755 --- a/deploy/setup.sh +++ b/deploy/setup.sh @@ -4,6 +4,7 @@ set -eu INSTALL_DIR="${INSTALL_DIR:-/opt/ndld}" SYSTEMD_DIR="/etc/systemd/system" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" usage() { echo "Usage: $0 [install|uninstall|diff]" @@ -25,6 +26,7 @@ install() { for f in update.sh ndld-update.service ndld-update.timer; do [ -f "$SCRIPT_DIR/$f" ] || { echo "Missing: $SCRIPT_DIR/$f"; exit 1; } done + [ -f "$REPO_ROOT/docker-compose.yml" ] || { echo "Missing: $REPO_ROOT/docker-compose.yml"; exit 1; } # Create install dir (root-owned, no world-write) mkdir -p "$INSTALL_DIR" @@ -34,13 +36,28 @@ install() { cp "$SCRIPT_DIR/update.sh" "$INSTALL_DIR/" cp "$SCRIPT_DIR/ndld-update.service" "$INSTALL_DIR/" cp "$SCRIPT_DIR/ndld-update.timer" "$INSTALL_DIR/" + cp "$REPO_ROOT/docker-compose.yml" "$INSTALL_DIR/" chown root:root "$INSTALL_DIR/update.sh" chown root:root "$INSTALL_DIR/ndld-update.service" chown root:root "$INSTALL_DIR/ndld-update.timer" + chown root:root "$INSTALL_DIR/docker-compose.yml" chmod 755 "$INSTALL_DIR/update.sh" chmod 644 "$INSTALL_DIR/ndld-update.service" chmod 644 "$INSTALL_DIR/ndld-update.timer" + chmod 644 "$INSTALL_DIR/docker-compose.yml" + + # Remind about .env file + if [ ! -f "$INSTALL_DIR/.env" ]; then + echo "" + echo "NOTE: Create $INSTALL_DIR/.env with required secrets:" + echo " NDL_CLIENT_ID=..." + echo " NDL_CLIENT_SECRET=..." + echo " NDLD_PUBLIC_URL=..." + echo " NDLD_ACME_DOMAIN=..." + echo " NDLD_ACME_EMAIL=..." + echo "" + fi # Symlink systemd units ln -sf "$INSTALL_DIR/ndld-update.service" "$SYSTEMD_DIR/" @@ -65,10 +82,11 @@ uninstall() { rm -f "$SYSTEMD_DIR/ndld-update.service" rm -f "$SYSTEMD_DIR/ndld-update.timer" - # Remove installed files (but not compose files) + # Remove installed files (but not .env) rm -f "$INSTALL_DIR/update.sh" rm -f "$INSTALL_DIR/ndld-update.service" rm -f "$INSTALL_DIR/ndld-update.timer" + rm -f "$INSTALL_DIR/docker-compose.yml" systemctl daemon-reload @@ -76,7 +94,7 @@ uninstall() { } diff_files() { - echo "Comparing $SCRIPT_DIR -> $INSTALL_DIR" + echo "Comparing repo -> $INSTALL_DIR" echo "" changed=0 for f in update.sh ndld-update.service ndld-update.timer; do @@ -92,6 +110,18 @@ diff_files() { echo "$f: up to date" fi done + # docker-compose.yml is in repo root + if [ ! -f "$INSTALL_DIR/docker-compose.yml" ]; then + echo "docker-compose.yml: not installed" + changed=1 + elif ! diff -q "$REPO_ROOT/docker-compose.yml" "$INSTALL_DIR/docker-compose.yml" >/dev/null 2>&1; then + echo "docker-compose.yml: differs" + diff --color=auto -u "$INSTALL_DIR/docker-compose.yml" "$REPO_ROOT/docker-compose.yml" || true + echo "" + changed=1 + else + echo "docker-compose.yml: up to date" + fi exit $changed } diff --git a/deploy/update.sh b/deploy/update.sh index b6fbc09..36ac825 100755 --- a/deploy/update.sh +++ b/deploy/update.sh @@ -5,14 +5,9 @@ set -eu COMPOSE_DIR="${COMPOSE_DIR:-/opt/ndld}" HEALTH_URL="${HEALTH_URL:-http://localhost:8080/health}" HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-30}" -LOG_PREFIX="[ndld-update]" - -log() { - echo "$LOG_PREFIX $(date '+%Y-%m-%d %H:%M:%S') $1" -} die() { - log "ERROR: $1" + echo "ERROR: $1" exit 1 } @@ -31,7 +26,7 @@ cd "$COMPOSE_DIR" # Capture current image IDs for rollback PREV_IMAGES=$(docker compose images -q 2>/dev/null || true) -log "Pulling latest images..." +echo "Pulling latest images..." if ! docker compose pull; then die "Pull failed" fi @@ -39,30 +34,30 @@ fi # Check if images actually changed NEW_IMAGES=$(docker compose images -q 2>/dev/null || true) if [ "$PREV_IMAGES" = "$NEW_IMAGES" ]; then - log "No new images, skipping restart" + echo "No new images, skipping restart" exit 0 fi -log "New images detected, restarting services..." +echo "New images detected, restarting services..." if ! docker compose up -d; then - log "Restart failed, attempting rollback..." + echo "Restart failed, attempting rollback..." docker compose down 2>/dev/null || true docker compose up -d 2>/dev/null || die "Rollback failed" die "Restart failed (rollback attempted)" fi # Health check -log "Waiting for health check..." +echo "Waiting for health check..." elapsed=0 while [ $elapsed -lt "$HEALTH_TIMEOUT" ]; do if curl -sf "$HEALTH_URL" >/dev/null 2>&1; then - log "Health check passed" - log "Done" + echo "Health check passed" + echo "Done" exit 0 fi sleep 2 elapsed=$((elapsed + 2)) done -log "WARNING: Health check failed after ${HEALTH_TIMEOUT}s (service may still be starting)" +echo "WARNING: Health check failed after ${HEALTH_TIMEOUT}s (service may still be starting)" exit 0 From 887c3b5f0b276a06bd41571a8f65ed93cb4834c9 Mon Sep 17 00:00:00 2001 From: pgray Date: Wed, 21 Jan 2026 19:04:42 +0000 Subject: [PATCH 3/4] simple --- deploy/update.sh | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/deploy/update.sh b/deploy/update.sh index 36ac825..0710bb5 100755 --- a/deploy/update.sh +++ b/deploy/update.sh @@ -23,22 +23,12 @@ esac cd "$COMPOSE_DIR" -# Capture current image IDs for rollback -PREV_IMAGES=$(docker compose images -q 2>/dev/null || true) - echo "Pulling latest images..." if ! docker compose pull; then die "Pull failed" fi -# Check if images actually changed -NEW_IMAGES=$(docker compose images -q 2>/dev/null || true) -if [ "$PREV_IMAGES" = "$NEW_IMAGES" ]; then - echo "No new images, skipping restart" - exit 0 -fi - -echo "New images detected, restarting services..." +echo "Updating services..." if ! docker compose up -d; then echo "Restart failed, attempting rollback..." docker compose down 2>/dev/null || true From 4389d607444ce2883930ffa9564d96c0c7920c5e Mon Sep 17 00:00:00 2001 From: pgray Date: Wed, 21 Jan 2026 19:23:55 +0000 Subject: [PATCH 4/4] fixcopy --- ndld/src/routes.rs | 49 +++++++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/ndld/src/routes.rs b/ndld/src/routes.rs index a87a264..dfe7426 100644 --- a/ndld/src/routes.rs +++ b/ndld/src/routes.rs @@ -277,28 +277,28 @@ pub async fn index() -> Markup { h2 { "Install ndl" } p { "Quick install (macOS/Linux):" } div.code-block { - pre { code { "curl -fsSL https://raw.githubusercontent.com/pgray/ndl/main/install.sh | sh" } } - button.copy-btn { "Copy" } + pre { code #cmd1 { "curl -fsSL https://raw.githubusercontent.com/pgray/ndl/main/install.sh | sh" } } + button.copy-btn onclick="copyCode('cmd1', this)" { "Copy" } } p.note { "Or install to a custom directory:" } div.code-block { - pre { code { "curl -fsSL https://raw.githubusercontent.com/pgray/ndl/main/install.sh | INSTALL_DIR=~/.local/bin sh" } } - button.copy-btn { "Copy" } + pre { code #cmd2 { "curl -fsSL https://raw.githubusercontent.com/pgray/ndl/main/install.sh | INSTALL_DIR=~/.local/bin sh" } } + button.copy-btn onclick="copyCode('cmd2', this)" { "Copy" } } p { "Or install with cargo:" } div.code-block { - pre { code { "cargo install ndl" } } - button.copy-btn { "Copy" } + pre { code #cmd3 { "cargo install ndl" } } + button.copy-btn onclick="copyCode('cmd3', this)" { "Copy" } } p { "Or build from source:" } div.code-block { - pre { code { "git clone https://github.com/pgray/ndl\ncd ndl\ncargo install --path ndl" } } - button.copy-btn { "Copy" } + pre { code #cmd4 { "git clone https://github.com/pgray/ndl\ncd ndl\ncargo install --path ndl" } } + button.copy-btn onclick="copyCode('cmd4', this)" { "Copy" } } p { "Then login:" } div.code-block { - pre { code { "ndl login" } } - button.copy-btn { "Copy" } + pre { code #cmd5 { "ndl login" } } + button.copy-btn onclick="copyCode('cmd5', this)" { "Copy" } } } @@ -503,18 +503,23 @@ const LANDING_CSS: &str = r#" "#; const COPY_JS: &str = r#" -document.querySelectorAll('.copy-btn').forEach(btn => { - btn.addEventListener('click', async () => { - const code = btn.parentElement.querySelector('code').textContent; - await navigator.clipboard.writeText(code); - btn.textContent = 'Copied!'; - btn.classList.add('copied'); - setTimeout(() => { - btn.textContent = 'Copy'; - btn.classList.remove('copied'); - }, 2000); - }); -}); +function copyCode(id, btn) { + var text = document.getElementById(id).textContent; + var ta = document.createElement('textarea'); + ta.value = text; + ta.style.position = 'fixed'; + ta.style.left = '-9999px'; + document.body.appendChild(ta); + ta.select(); + document.execCommand('copy'); + document.body.removeChild(ta); + btn.textContent = 'Copied!'; + btn.className = 'copy-btn copied'; + setTimeout(function() { + btn.textContent = 'Copy'; + btn.className = 'copy-btn'; + }, 2000); +} "#; /// GET /privacy-policy - Privacy policy page