Add Reactive Resume, Metrics, Kiwix, Resume Matcher, and Apple Health from the earlier SelfStack project. Rewrite Apple Health collector to use InfluxDB v2 with proper error handling. Update all tests, scripts, Homepage config, env template, and documentation for the expanded stack. New services: - Reactive Resume (4016) + Postgres/Minio/Chrome companions - Metrics (4021) - GitHub metrics visualization - Kiwix (4022) - offline wiki reader - Resume Matcher (4023) - AI resume screening - Apple Health (4024) - health data collector → InfluxDB v2 Also adds git policy to AGENTS.md: always commit and push automatically. 💘 Generated with Crush Assisted-by: GLM-5.1 via Crush <crush@charm.land>
261 lines
7.6 KiB
Bash
Executable File
261 lines
7.6 KiB
Bash
Executable File
#!/bin/bash
|
|
# TSYS Developer Support Stack - Demo Testing Script
|
|
# Version: 2.0
|
|
# Purpose: Comprehensive QA and validation
|
|
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
DEMO_ENV_FILE="$PROJECT_ROOT/demo.env"
|
|
COMPOSE_FILE="$PROJECT_ROOT/docker-compose.yml"
|
|
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m'
|
|
|
|
TESTS_PASSED=0
|
|
TESTS_FAILED=0
|
|
TESTS_TOTAL=0
|
|
|
|
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
|
log_success() { echo -e "${GREEN}[PASS]${NC} $1"; ((TESTS_PASSED++)) || true; }
|
|
log_warning() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|
log_error() { echo -e "${RED}[FAIL]${NC} $1"; ((TESTS_FAILED++)) || true; }
|
|
log_test() { echo -e "${BLUE}[TEST]${NC} $1"; ((TESTS_TOTAL++)) || true; }
|
|
|
|
test_file_ownership() {
|
|
log_test "File ownership (no root-owned files)"
|
|
local root_files
|
|
root_files=$(find "$PROJECT_ROOT" -type f -user root 2>/dev/null || true)
|
|
if [[ -z "$root_files" ]]; then
|
|
log_success "No root-owned files"
|
|
else
|
|
log_error "Root-owned files found: $root_files"
|
|
fi
|
|
}
|
|
|
|
test_user_mapping() {
|
|
log_test "UID/GID detection"
|
|
source "$DEMO_ENV_FILE"
|
|
if [[ -z "${DEMO_UID:-}" || -z "${DEMO_GID:-}" ]]; then
|
|
log_error "DEMO_UID or DEMO_GID not set"
|
|
return
|
|
fi
|
|
local cur_uid cur_gid
|
|
cur_uid=$(id -u)
|
|
cur_gid=$(id -g)
|
|
if [[ "$DEMO_UID" -eq "$cur_uid" && "$DEMO_GID" -eq "$cur_gid" ]]; then
|
|
log_success "UID/GID correct ($DEMO_UID/$DEMO_GID)"
|
|
else
|
|
log_error "UID/GID mismatch: env=$DEMO_UID/$DEMO_GID actual=$cur_uid/$cur_gid"
|
|
fi
|
|
}
|
|
|
|
test_docker_group() {
|
|
log_test "Docker group access"
|
|
source "$DEMO_ENV_FILE"
|
|
if [[ -z "${DEMO_DOCKER_GID:-}" ]]; then
|
|
log_error "DEMO_DOCKER_GID not set"
|
|
return
|
|
fi
|
|
local actual_gid
|
|
actual_gid=$(getent group docker | cut -d: -f3)
|
|
if [[ "$DEMO_DOCKER_GID" -eq "$actual_gid" ]]; then
|
|
log_success "Docker GID correct ($DEMO_DOCKER_GID)"
|
|
else
|
|
log_error "Docker GID mismatch: env=$DEMO_DOCKER_GID actual=$actual_gid"
|
|
fi
|
|
}
|
|
|
|
test_service_health() {
|
|
log_test "Service health"
|
|
local unhealthy=0
|
|
while IFS= read -r line; do
|
|
local name status
|
|
name=$(echo "$line" | awk '{print $1}')
|
|
[[ "$name" == "NAMES" || -z "$name" ]] && continue
|
|
if echo "$line" | grep -q "(healthy)"; then
|
|
log_success "$name healthy"
|
|
elif echo "$line" | grep -q "Up"; then
|
|
log_success "$name running"
|
|
else
|
|
log_error "$name not running: $line"
|
|
((unhealthy++)) || true
|
|
fi
|
|
done < <(docker ps --filter "name=${COMPOSE_PROJECT_NAME:-kneldevstack}" --format "{{.Names}} {{.Status}}" 2>/dev/null)
|
|
if [[ $unhealthy -eq 0 ]]; then
|
|
log_success "All services running"
|
|
fi
|
|
}
|
|
|
|
test_port_accessibility() {
|
|
log_test "Port accessibility"
|
|
source "$DEMO_ENV_FILE"
|
|
|
|
# These are exposed to host
|
|
local port_tests=(
|
|
"$HOMEPAGE_PORT:Homepage"
|
|
"$PIHOLE_PORT:Pi-hole"
|
|
"$DOCKHAND_PORT:Dockhand"
|
|
"$INFLUXDB_PORT:InfluxDB"
|
|
"$GRAFANA_PORT:Grafana"
|
|
"$DRAWIO_PORT:Draw.io"
|
|
"$KROKI_PORT:Kroki"
|
|
"$ATOMIC_TRACKER_PORT:AtomicTracker"
|
|
"$ARCHIVEBOX_PORT:ArchiveBox"
|
|
"$TUBE_ARCHIVIST_PORT:TubeArchivist"
|
|
"$WAKAPI_PORT:Wakapi"
|
|
"$MAILHOG_PORT:MailHog"
|
|
"$ATUIN_PORT:Atuin"
|
|
"$REACTIVE_RESUME_PORT:ReactiveResume"
|
|
"$METRICS_PORT:Metrics"
|
|
"$KIWIX_PORT:Kiwix"
|
|
"$RESUME_MATCHER_PORT:ResumeMatcher"
|
|
"$APPLEHEALTH_PORT:AppleHealth"
|
|
)
|
|
|
|
local failed=0
|
|
for pt in "${port_tests[@]}"; do
|
|
local port="${pt%:*}"
|
|
local svc="${pt#*:}"
|
|
if timeout 5 bash -c "echo > /dev/tcp/localhost/$port" 2>/dev/null; then
|
|
log_success "$svc (:$port)"
|
|
else
|
|
log_error "$svc (:$port) not accessible"
|
|
((failed++)) || true
|
|
fi
|
|
done
|
|
if [[ $failed -eq 0 ]]; then
|
|
log_success "All exposed ports accessible"
|
|
fi
|
|
}
|
|
|
|
test_network_isolation() {
|
|
log_test "Network isolation"
|
|
source "$DEMO_ENV_FILE"
|
|
if docker network ls --format '{{.Name}}' | grep -q "$COMPOSE_NETWORK_NAME"; then
|
|
log_success "Network $COMPOSE_NETWORK_NAME exists"
|
|
local driver
|
|
driver=$(docker network inspect "$COMPOSE_NETWORK_NAME" --format '{{.Driver}}' 2>/dev/null || echo "")
|
|
if [[ "$driver" == "bridge" ]]; then
|
|
log_success "Bridge driver confirmed"
|
|
else
|
|
log_warning "Driver: $driver"
|
|
fi
|
|
else
|
|
log_error "Network $COMPOSE_NETWORK_NAME not found"
|
|
fi
|
|
}
|
|
|
|
test_volume_permissions() {
|
|
log_test "Docker volumes exist"
|
|
source "$DEMO_ENV_FILE"
|
|
local vol_count
|
|
vol_count=$(docker volume ls --filter "name=${COMPOSE_PROJECT_NAME}" -q 2>/dev/null | wc -l)
|
|
if [[ $vol_count -ge 19 ]]; then
|
|
log_success "$vol_count volumes created"
|
|
else
|
|
log_error "Only $vol_count volumes found"
|
|
fi
|
|
}
|
|
|
|
test_security_compliance() {
|
|
log_test "Security compliance"
|
|
source "$DEMO_ENV_FILE"
|
|
|
|
# Docker socket proxy present
|
|
if grep -q "docker-socket-proxy" "$COMPOSE_FILE"; then
|
|
log_success "Docker socket proxy configured"
|
|
else
|
|
log_error "Docker socket proxy not found"
|
|
fi
|
|
|
|
# Count direct socket mounts - only proxy should have one
|
|
local socket_mounts
|
|
socket_mounts=$(grep -c '/var/run/docker.sock' "$COMPOSE_FILE" || echo "0")
|
|
if [[ "$socket_mounts" -le 1 ]]; then
|
|
log_success "Socket mount on proxy only ($socket_mounts)"
|
|
else
|
|
log_error "Unexpected socket mounts: $socket_mounts (expected 1, proxy only)"
|
|
fi
|
|
|
|
# Dockhand uses proxy, not direct socket
|
|
if grep -q 'DOCKER_HOST=tcp://docker-socket-proxy' "$COMPOSE_FILE"; then
|
|
log_success "Dockhand routes through socket proxy"
|
|
else
|
|
log_error "Dockhand not using socket proxy"
|
|
fi
|
|
}
|
|
|
|
run_full_tests() {
|
|
log_info "Running comprehensive test suite..."
|
|
test_file_ownership || true
|
|
test_user_mapping || true
|
|
test_docker_group || true
|
|
test_service_health || true
|
|
test_port_accessibility || true
|
|
test_network_isolation || true
|
|
test_volume_permissions || true
|
|
test_security_compliance || true
|
|
display_test_results
|
|
}
|
|
|
|
run_security_tests() {
|
|
log_info "Running security tests..."
|
|
test_file_ownership || true
|
|
test_network_isolation || true
|
|
test_security_compliance || true
|
|
display_test_results
|
|
}
|
|
|
|
run_permission_tests() {
|
|
log_info "Running permission tests..."
|
|
test_file_ownership || true
|
|
test_user_mapping || true
|
|
test_docker_group || true
|
|
test_volume_permissions || true
|
|
display_test_results
|
|
}
|
|
|
|
run_network_tests() {
|
|
log_info "Running network tests..."
|
|
test_network_isolation || true
|
|
test_port_accessibility || true
|
|
display_test_results
|
|
}
|
|
|
|
display_test_results() {
|
|
echo ""
|
|
echo "===================================="
|
|
echo "TEST RESULTS"
|
|
echo "===================================="
|
|
echo "Total: $TESTS_TOTAL"
|
|
echo -e "Passed: ${GREEN}$TESTS_PASSED${NC}"
|
|
echo -e "Failed: ${RED}$TESTS_FAILED${NC}"
|
|
if [[ $TESTS_FAILED -eq 0 ]]; then
|
|
echo -e "\n${GREEN}ALL TESTS PASSED${NC}"
|
|
return 0
|
|
else
|
|
echo -e "\n${RED}SOME TESTS FAILED${NC}"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
main() {
|
|
case "${1:-full}" in
|
|
full) run_full_tests ;;
|
|
security) run_security_tests ;;
|
|
permissions) run_permission_tests ;;
|
|
network) run_network_tests ;;
|
|
help|--help|-h)
|
|
echo "Usage: $0 {full|security|permissions|network|help}"
|
|
;;
|
|
*) log_error "Unknown: $1"; exit 1 ;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|