feat: add OS compatibility checks before running platform-specific logic
- lib/common.sh: add require_local_os, require_remote_os, require_remote_pkg_manager - setup/macbook.sh: require macOS (Darwin) - setup/unraid.sh: require remote is Linux - setup/fedora.sh: require remote is Linux + has dnf (RPM-based) - manage_runner.sh: native runner add/remove requires macOS - run_all.sh: control plane must be macOS - preflight.sh: 3 new checks (1: local=macOS, 2: Unraid=Linux, 3: Fedora=Linux+dnf) - phase5_migrate_pipelines.sh: fix sed -i to be portable (no macOS-only syntax) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -123,6 +123,55 @@ require_vars() {
|
||||
done
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# OS compatibility checks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Verify the local machine is running the expected OS.
|
||||
# Usage: require_local_os "Darwin" "This script requires macOS"
|
||||
# os_type: "Darwin" for macOS, "Linux" for Linux
|
||||
# Checks `uname -s` against the expected value.
|
||||
require_local_os() {
|
||||
local expected="$1" msg="${2:-This script requires ${1}}"
|
||||
local actual
|
||||
actual="$(uname -s)"
|
||||
if [[ "$actual" != "$expected" ]]; then
|
||||
log_error "$msg"
|
||||
log_error "Detected OS: $actual (expected: $expected)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Verify a remote machine (via SSH) is running the expected OS.
|
||||
# Usage: require_remote_os "UNRAID" "Linux" "Unraid must be a Linux machine"
|
||||
# host_key: SSH host key (UNRAID, FEDORA) — uses ssh_exec
|
||||
# os_type: expected `uname -s` output
|
||||
require_remote_os() {
|
||||
local host_key="$1" expected="$2" msg="${3:-Remote host $1 must be running $2}"
|
||||
local actual
|
||||
actual="$(ssh_exec "$host_key" "uname -s" 2>/dev/null)" || {
|
||||
log_error "Cannot determine OS on ${host_key} (SSH failed)"
|
||||
return 1
|
||||
}
|
||||
if [[ "$actual" != "$expected" ]]; then
|
||||
log_error "$msg"
|
||||
log_error "${host_key} detected OS: $actual (expected: $expected)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if a remote host has a specific package manager available.
|
||||
# Usage: require_remote_pkg_manager "FEDORA" "dnf" "Fedora requires dnf"
|
||||
require_remote_pkg_manager() {
|
||||
local host_key="$1" pkg_mgr="$2"
|
||||
local msg="${3:-${host_key} requires ${pkg_mgr}}"
|
||||
if ! ssh_exec "$host_key" "command -v $pkg_mgr" &>/dev/null; then
|
||||
log_error "$msg"
|
||||
log_error "${host_key} does not have '${pkg_mgr}' — is this the right machine?"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SSH
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -189,6 +189,9 @@ add_docker_runner() {
|
||||
# and unreliable for long-running background services.
|
||||
# ---------------------------------------------------------------------------
|
||||
add_native_runner() {
|
||||
# Native runners use launchctl + macOS-specific paths — must be macOS
|
||||
require_local_os "Darwin" "Native runner deployment requires macOS (uses launchctl)"
|
||||
|
||||
log_info "Deploying native runner '${RUNNER_NAME}' on local machine..."
|
||||
|
||||
# Resolve ~ to actual home directory for local execution
|
||||
@@ -278,6 +281,9 @@ remove_docker_runner() {
|
||||
# remove_native_runner — Unload launchd service + remove binary + plist
|
||||
# ---------------------------------------------------------------------------
|
||||
remove_native_runner() {
|
||||
# Native runners use launchctl + macOS-specific paths — must be macOS
|
||||
require_local_os "Darwin" "Native runner removal requires macOS (uses launchctl)"
|
||||
|
||||
log_info "Removing native runner '${RUNNER_NAME}' from local machine..."
|
||||
|
||||
# Resolve ~ to actual home directory
|
||||
|
||||
@@ -114,13 +114,16 @@ for repo in "${REPOS[@]}"; do
|
||||
mv "$tmpwf" "$dest"
|
||||
|
||||
# Replace GitHub-specific context variables with Gitea equivalents
|
||||
# Using sed -i '' for macOS compatibility (GNU sed uses -i without arg)
|
||||
sed -i '' \
|
||||
# Using sed with a temp file for portability (macOS sed -i requires '',
|
||||
# GNU sed -i requires no arg — avoiding both by writing to a temp file)
|
||||
tmpwf=$(mktemp)
|
||||
sed \
|
||||
-e 's/github\.repository/gitea.repository/g' \
|
||||
-e 's/github\.event/gitea.event/g' \
|
||||
-e 's/github\.token/gitea.token/g' \
|
||||
-e 's/github\.server_url/gitea.server_url/g' \
|
||||
"$dest"
|
||||
"$dest" > "$tmpwf"
|
||||
mv "$tmpwf" "$dest"
|
||||
|
||||
log_info " Migrated: ${local_name}"
|
||||
done
|
||||
|
||||
113
preflight.sh
113
preflight.sh
@@ -16,7 +16,7 @@ FAIL_COUNT=0
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check helper — runs a check function, tracks pass/fail count.
|
||||
# Intentionally does NOT exit on failure — we want to run ALL 16 checks
|
||||
# Intentionally does NOT exit on failure — we want to run ALL 19 checks
|
||||
# so the user sees every issue at once, not one at a time.
|
||||
# ---------------------------------------------------------------------------
|
||||
check() {
|
||||
@@ -33,12 +33,41 @@ check() {
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check 1: .env exists
|
||||
# Check 1: Local machine is macOS (control plane uses brew, launchctl, macOS sed)
|
||||
# ---------------------------------------------------------------------------
|
||||
check_local_os() {
|
||||
[[ "$(uname -s)" == "Darwin" ]]
|
||||
}
|
||||
check 1 "Local machine is macOS (control plane)" check_local_os
|
||||
if ! check_local_os 2>/dev/null; then
|
||||
log_error " → This toolkit is designed to run from macOS. Detected: $(uname -s)"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check 2: Unraid is Linux (via SSH)
|
||||
# ---------------------------------------------------------------------------
|
||||
check_unraid_os() {
|
||||
local remote_os
|
||||
remote_os="$(ssh_exec UNRAID "uname -s" 2>/dev/null)" || return 1
|
||||
[[ "$remote_os" == "Linux" ]]
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check 3: Fedora is Linux with dnf (RPM-based)
|
||||
# ---------------------------------------------------------------------------
|
||||
check_fedora_os() {
|
||||
local remote_os
|
||||
remote_os="$(ssh_exec FEDORA "uname -s" 2>/dev/null)" || return 1
|
||||
[[ "$remote_os" == "Linux" ]] && ssh_exec FEDORA "command -v dnf" &>/dev/null
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check 4: .env exists
|
||||
# ---------------------------------------------------------------------------
|
||||
check_env_exists() {
|
||||
[[ -f "${SCRIPT_DIR}/.env" ]]
|
||||
}
|
||||
check 1 ".env file exists" check_env_exists
|
||||
check 4 ".env file exists" check_env_exists
|
||||
if [[ ! -f "${SCRIPT_DIR}/.env" ]]; then
|
||||
log_error " → .env not found. Copy .env.example to .env and fill in values."
|
||||
log_error " → Or run: setup/configure_env.sh"
|
||||
@@ -46,12 +75,12 @@ if [[ ! -f "${SCRIPT_DIR}/.env" ]]; then
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check 2: runners.conf exists
|
||||
# Check 5: runners.conf exists
|
||||
# ---------------------------------------------------------------------------
|
||||
check_runners_conf() {
|
||||
[[ -f "${SCRIPT_DIR}/runners.conf" ]]
|
||||
}
|
||||
check 2 "runners.conf file exists" check_runners_conf
|
||||
check 5 "runners.conf file exists" check_runners_conf
|
||||
if [[ ! -f "${SCRIPT_DIR}/runners.conf" ]]; then
|
||||
log_error " → runners.conf not found. Copy runners.conf.example to runners.conf."
|
||||
fi
|
||||
@@ -64,7 +93,7 @@ if [[ -f "${SCRIPT_DIR}/.env" ]]; then
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check 3: Required .env vars
|
||||
# Check 6: Required .env vars
|
||||
# ---------------------------------------------------------------------------
|
||||
REQUIRED_VARS=(
|
||||
UNRAID_IP UNRAID_SSH_USER UNRAID_GITEA_DATA_PATH
|
||||
@@ -89,76 +118,90 @@ check_required_vars() {
|
||||
done
|
||||
return $missing
|
||||
}
|
||||
check 3 "All required .env vars are set" check_required_vars
|
||||
check 6 "All required .env vars are set" check_required_vars
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check 4: SSH to Unraid
|
||||
# Check 7: SSH to Unraid
|
||||
# ---------------------------------------------------------------------------
|
||||
check_ssh_unraid() {
|
||||
ssh_check UNRAID
|
||||
}
|
||||
check 4 "SSH to Unraid (${UNRAID_IP:-<not set>})" check_ssh_unraid
|
||||
if [[ $FAIL_COUNT -gt 0 ]] && ! ssh_check UNRAID 2>/dev/null; then
|
||||
check 7 "SSH to Unraid (${UNRAID_IP:-<not set>})" check_ssh_unraid
|
||||
if ! ssh_check UNRAID 2>/dev/null; then
|
||||
log_error " → Cannot SSH to Unraid. Run setup/unraid.sh or check SSH config."
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check 5: SSH to Fedora
|
||||
# Check 8: SSH to Fedora
|
||||
# ---------------------------------------------------------------------------
|
||||
check_ssh_fedora() {
|
||||
ssh_check FEDORA
|
||||
}
|
||||
check 5 "SSH to Fedora (${FEDORA_IP:-<not set>})" check_ssh_fedora
|
||||
check 8 "SSH to Fedora (${FEDORA_IP:-<not set>})" check_ssh_fedora
|
||||
if ! ssh_check FEDORA 2>/dev/null; then
|
||||
log_error " → Cannot SSH to Fedora. Run setup/fedora.sh or check SSH config."
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check 6: Docker on Unraid
|
||||
# Checks 2-3: Remote OS checks (deferred until after SSH is confirmed)
|
||||
# These are numbered 2-3 in the output but run after SSH checks because
|
||||
# they require SSH connectivity to `uname -s` on the remote machines.
|
||||
# ---------------------------------------------------------------------------
|
||||
check 2 "Unraid is Linux" check_unraid_os
|
||||
if ! check_unraid_os 2>/dev/null; then
|
||||
log_error " → UNRAID_IP points to a non-Linux machine. Check your .env."
|
||||
fi
|
||||
check 3 "Fedora is Linux with dnf (RPM-based)" check_fedora_os
|
||||
if ! check_fedora_os 2>/dev/null; then
|
||||
log_error " → FEDORA_IP points to a machine that isn't RPM-based Linux. Check your .env."
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check 9: Docker on Unraid
|
||||
# ---------------------------------------------------------------------------
|
||||
check_docker_unraid() {
|
||||
ssh_exec UNRAID "docker --version" &>/dev/null
|
||||
}
|
||||
check 6 "Docker available on Unraid" check_docker_unraid
|
||||
check 9 "Docker available on Unraid" check_docker_unraid
|
||||
if ! check_docker_unraid 2>/dev/null; then
|
||||
log_error " → Docker not found on Unraid. Run setup/unraid.sh."
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check 7: Docker on Fedora
|
||||
# Check 10: Docker on Fedora
|
||||
# ---------------------------------------------------------------------------
|
||||
check_docker_fedora() {
|
||||
ssh_exec FEDORA "docker --version" &>/dev/null
|
||||
}
|
||||
check 7 "Docker available on Fedora" check_docker_fedora
|
||||
check 10 "Docker available on Fedora" check_docker_fedora
|
||||
if ! check_docker_fedora 2>/dev/null; then
|
||||
log_error " → Docker not found on Fedora. Run setup/fedora.sh."
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check 8: docker-compose on Unraid
|
||||
# Check 11: docker-compose on Unraid
|
||||
# ---------------------------------------------------------------------------
|
||||
check_compose_unraid() {
|
||||
ssh_exec UNRAID "docker compose version" &>/dev/null || ssh_exec UNRAID "docker-compose --version" &>/dev/null
|
||||
}
|
||||
check 8 "docker-compose available on Unraid" check_compose_unraid
|
||||
check 11 "docker-compose available on Unraid" check_compose_unraid
|
||||
if ! check_compose_unraid 2>/dev/null; then
|
||||
log_error " → docker-compose not found on Unraid. Run setup/unraid.sh."
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check 9: docker-compose on Fedora
|
||||
# Check 12: docker-compose on Fedora
|
||||
# ---------------------------------------------------------------------------
|
||||
check_compose_fedora() {
|
||||
ssh_exec FEDORA "docker compose version" &>/dev/null || ssh_exec FEDORA "docker-compose --version" &>/dev/null
|
||||
}
|
||||
check 9 "docker-compose available on Fedora" check_compose_fedora
|
||||
check 12 "docker-compose available on Fedora" check_compose_fedora
|
||||
if ! check_compose_fedora 2>/dev/null; then
|
||||
log_error " → docker-compose not found on Fedora. Run setup/fedora.sh."
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check 10: Port free on Unraid
|
||||
# Check 13: Port free on Unraid
|
||||
# Uses ss (socket statistics) to check if any process is listening on the port.
|
||||
# The ! negates the grep — we PASS if the port is NOT found in use.
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -166,49 +209,49 @@ check_port_unraid() {
|
||||
local port="${UNRAID_GITEA_PORT:-3000}"
|
||||
! ssh_exec UNRAID "ss -tlnp | grep -q ':${port} '" 2>/dev/null
|
||||
}
|
||||
check 10 "Port ${UNRAID_GITEA_PORT:-3000} free on Unraid" check_port_unraid
|
||||
check 13 "Port ${UNRAID_GITEA_PORT:-3000} free on Unraid" check_port_unraid
|
||||
if ! check_port_unraid 2>/dev/null; then
|
||||
log_error " → Port ${UNRAID_GITEA_PORT:-3000} already in use on Unraid."
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check 11: Port free on Fedora
|
||||
# Check 14: Port free on Fedora
|
||||
# ---------------------------------------------------------------------------
|
||||
check_port_fedora() {
|
||||
local port="${FEDORA_GITEA_PORT:-3000}"
|
||||
! ssh_exec FEDORA "ss -tlnp | grep -q ':${port} '" 2>/dev/null
|
||||
}
|
||||
check 11 "Port ${FEDORA_GITEA_PORT:-3000} free on Fedora" check_port_fedora
|
||||
check 14 "Port ${FEDORA_GITEA_PORT:-3000} free on Fedora" check_port_fedora
|
||||
if ! check_port_fedora 2>/dev/null; then
|
||||
log_error " → Port ${FEDORA_GITEA_PORT:-3000} already in use on Fedora."
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check 12: DNS resolves
|
||||
# Check 15: DNS resolves
|
||||
# ---------------------------------------------------------------------------
|
||||
check_dns() {
|
||||
local resolved
|
||||
resolved=$(dig +short "${GITEA_DOMAIN:-}" 2>/dev/null | head -1)
|
||||
[[ "$resolved" == "${UNRAID_IP:-}" ]]
|
||||
}
|
||||
check 12 "DNS: ${GITEA_DOMAIN:-<not set>} resolves to ${UNRAID_IP:-<not set>}" check_dns
|
||||
check 15 "DNS: ${GITEA_DOMAIN:-<not set>} resolves to ${UNRAID_IP:-<not set>}" check_dns
|
||||
if ! check_dns 2>/dev/null; then
|
||||
log_error " → ${GITEA_DOMAIN:-GITEA_DOMAIN} does not resolve to ${UNRAID_IP:-UNRAID_IP}."
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check 13: GitHub token valid
|
||||
# Check 16: GitHub token valid
|
||||
# ---------------------------------------------------------------------------
|
||||
check_github_token() {
|
||||
[[ -n "${GITHUB_TOKEN:-}" ]] && curl -sf -H "Authorization: token ${GITHUB_TOKEN}" https://api.github.com/user -o /dev/null
|
||||
}
|
||||
check 13 "GitHub token valid" check_github_token
|
||||
check 16 "GitHub token valid" check_github_token
|
||||
if ! check_github_token 2>/dev/null; then
|
||||
log_error " → GitHub token invalid. Check GITHUB_TOKEN in .env."
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check 14: GitHub repos exist
|
||||
# Check 17: GitHub repos exist
|
||||
# ---------------------------------------------------------------------------
|
||||
check_github_repos() {
|
||||
local all_ok=0
|
||||
@@ -224,28 +267,28 @@ check_github_repos() {
|
||||
done
|
||||
return $all_ok
|
||||
}
|
||||
check 14 "All GitHub repos exist" check_github_repos
|
||||
check 17 "All GitHub repos exist" check_github_repos
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check 15: Nginx running on Unraid
|
||||
# Check 18: Nginx running on Unraid
|
||||
# ---------------------------------------------------------------------------
|
||||
check_nginx() {
|
||||
local status
|
||||
status=$(ssh_exec UNRAID "docker ps --filter name=${NGINX_CONTAINER_NAME:-nginx} --format '{{.Status}}'" 2>/dev/null)
|
||||
[[ "$status" == *"Up"* ]]
|
||||
}
|
||||
check 15 "Nginx container '${NGINX_CONTAINER_NAME:-<not set>}' running on Unraid" check_nginx
|
||||
check 18 "Nginx container '${NGINX_CONTAINER_NAME:-<not set>}' running on Unraid" check_nginx
|
||||
if ! check_nginx 2>/dev/null; then
|
||||
log_error " → Nginx container '${NGINX_CONTAINER_NAME:-}' not running on Unraid."
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check 16: Nginx conf dir writable
|
||||
# Check 19: Nginx conf dir writable
|
||||
# ---------------------------------------------------------------------------
|
||||
check_nginx_conf() {
|
||||
ssh_exec UNRAID "test -w '${NGINX_CONF_PATH:-/nonexistent}'" 2>/dev/null
|
||||
}
|
||||
check 16 "Nginx config path writable (${NGINX_CONF_PATH:-<not set>})" check_nginx_conf
|
||||
check 19 "Nginx config path writable (${NGINX_CONF_PATH:-<not set>})" check_nginx_conf
|
||||
if ! check_nginx_conf 2>/dev/null; then
|
||||
log_error " → Nginx config path ${NGINX_CONF_PATH:-} not writable on Unraid."
|
||||
fi
|
||||
@@ -254,7 +297,7 @@ fi
|
||||
# Summary
|
||||
# ---------------------------------------------------------------------------
|
||||
printf '\n'
|
||||
log_info "Results: ${PASS_COUNT} passed, ${FAIL_COUNT} failed (out of 16 checks)"
|
||||
log_info "Results: ${PASS_COUNT} passed, ${FAIL_COUNT} failed (out of 19 checks)"
|
||||
|
||||
if [[ $FAIL_COUNT -gt 0 ]]; then
|
||||
log_error "Preflight FAILED — fix the issues above before proceeding."
|
||||
|
||||
@@ -16,6 +16,11 @@ set -euo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
source "${SCRIPT_DIR}/lib/common.sh"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# OS check — the control plane must be macOS (uses brew, launchctl, macOS sed)
|
||||
# ---------------------------------------------------------------------------
|
||||
require_local_os "Darwin" "run_all.sh must run from macOS (the control plane)"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parse arguments
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -13,6 +13,15 @@ require_vars FEDORA_IP FEDORA_SSH_USER FEDORA_GITEA_DATA_PATH
|
||||
|
||||
log_info "=== Fedora Setup ==="
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# OS check — must be Linux with dnf (RPM-based distro like Fedora/RHEL/CentOS)
|
||||
# This script uses `dnf` for all package installs. Running it against a
|
||||
# Debian/Ubuntu/Arch host would fail with confusing errors.
|
||||
# --------------------------------------------------------------------------
|
||||
log_info "Verifying Fedora OS..."
|
||||
require_remote_os "FEDORA" "Linux" "Fedora target must be a Linux machine — check FEDORA_IP in .env"
|
||||
require_remote_pkg_manager "FEDORA" "dnf" "Fedora target must have dnf (RPM-based distro) — this script won't work on Debian/Ubuntu"
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# SSH connectivity
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
@@ -10,6 +10,11 @@ source "${SCRIPT_DIR}/../lib/common.sh"
|
||||
|
||||
log_info "=== MacBook Setup ==="
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# OS check — this script uses Homebrew, launchctl, Xcode tools (macOS only)
|
||||
# --------------------------------------------------------------------------
|
||||
require_local_os "Darwin" "macbook.sh must run on macOS — detected a non-macOS system"
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Homebrew
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
@@ -13,6 +13,12 @@ require_vars UNRAID_IP UNRAID_SSH_USER UNRAID_GITEA_DATA_PATH
|
||||
|
||||
log_info "=== Unraid Setup ==="
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# OS check — Unraid must be Linux (Docker, static binaries, x86_64)
|
||||
# --------------------------------------------------------------------------
|
||||
log_info "Verifying Unraid OS..."
|
||||
require_remote_os "UNRAID" "Linux" "Unraid must be a Linux machine — check UNRAID_IP in .env"
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# SSH connectivity
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user