Replace Portainer container management service with Dockhand: - Update docker-compose.yml.template with Dockhand service definition - Replace portainer_data volume with dockhand_data - Update PORTAINER_PORT to DOCKHAND_PORT in demo.env - Update all script references (demo-stack.sh, demo-test.sh, validate-all.sh) - Update integration test from Portainer to Dockhand - Update documentation files (README.md, AGENTS.md, api-docs, service-guides, troubleshooting) Dockhand provides modern Docker management UI with: - Container lifecycle management - Compose stack orchestration - Git-based deployments - Multi-environment support - Terminal access and log streaming - File browser capabilities Maintains same port (4007) for consistency. 💘 Generated with Crush Assisted-by: GLM-4.7 via Crush <crush@charm.land>
448 lines
13 KiB
Bash
Executable File
448 lines
13 KiB
Bash
Executable File
#!/bin/bash
|
|
# TSYS Developer Support Stack - Demo Testing Script
|
|
# Version: 1.0
|
|
# Purpose: Comprehensive QA and validation
|
|
|
|
set -euo pipefail
|
|
|
|
# Script Configuration
|
|
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"
|
|
|
|
# Color Codes for Output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# Test Results
|
|
TESTS_PASSED=0
|
|
TESTS_FAILED=0
|
|
TESTS_TOTAL=0
|
|
|
|
# Logging Functions
|
|
log_info() {
|
|
echo -e "${BLUE}[INFO]${NC} $1"
|
|
}
|
|
|
|
log_success() {
|
|
echo -e "${GREEN}[PASS]${NC} $1"
|
|
((TESTS_PASSED++))
|
|
}
|
|
|
|
log_warning() {
|
|
echo -e "${YELLOW}[WARNING]${NC} $1"
|
|
}
|
|
|
|
log_error() {
|
|
echo -e "${RED}[FAIL]${NC} $1"
|
|
((TESTS_FAILED++))
|
|
}
|
|
|
|
log_test() {
|
|
echo -e "${BLUE}[TEST]${NC} $1"
|
|
((TESTS_TOTAL++))
|
|
}
|
|
|
|
# Function to test file ownership
|
|
test_file_ownership() {
|
|
log_test "Testing file ownership (no root-owned files)..."
|
|
|
|
local project_root_files
|
|
project_root_files=$(find "$PROJECT_ROOT" -type f -user root 2>/dev/null || true)
|
|
|
|
if [[ -z "$project_root_files" ]]; then
|
|
log_success "No root-owned files found in project directory"
|
|
else
|
|
log_error "Root-owned files found:"
|
|
echo "$project_root_files"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Function to test user mapping
|
|
test_user_mapping() {
|
|
log_test "Testing UID/GID detection and application..."
|
|
|
|
# Source environment variables
|
|
# shellcheck disable=SC1090,SC1091
|
|
source "$DEMO_ENV_FILE"
|
|
|
|
# Check if UID/GID are set
|
|
if [[ -z "$DEMO_UID" || -z "$DEMO_GID" ]]; then
|
|
log_error "DEMO_UID or DEMO_GID not set in demo.env"
|
|
return 1
|
|
fi
|
|
|
|
# Check if values match current user
|
|
local current_uid
|
|
local current_gid
|
|
current_uid=$(id -u)
|
|
current_gid=$(id -g)
|
|
|
|
if [[ "$DEMO_UID" -eq "$current_uid" && "$DEMO_GID" -eq "$current_gid" ]]; then
|
|
log_success "UID/GID correctly detected and applied (UID: $DEMO_UID, GID: $DEMO_GID)"
|
|
else
|
|
log_error "UID/GID mismatch. Expected: $current_uid/$current_gid, Found: $DEMO_UID/$DEMO_GID"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Function to test Docker group access
|
|
test_docker_group() {
|
|
log_test "Testing Docker group access..."
|
|
|
|
# shellcheck disable=SC1090,SC1091
|
|
source "$DEMO_ENV_FILE"
|
|
|
|
if [[ -z "$DEMO_DOCKER_GID" ]]; then
|
|
log_error "DEMO_DOCKER_GID not set in demo.env"
|
|
return 1
|
|
fi
|
|
|
|
# Check if docker group exists
|
|
if getent group docker >/dev/null 2>&1; then
|
|
local docker_gid
|
|
docker_gid=$(getent group docker | cut -d: -f3)
|
|
if [[ "$DEMO_DOCKER_GID" -eq "$docker_gid" ]]; then
|
|
log_success "Docker group ID correctly detected (GID: $DEMO_DOCKER_GID)"
|
|
else
|
|
log_error "Docker group ID mismatch. Expected: $docker_gid, Found: $DEMO_DOCKER_GID"
|
|
return 1
|
|
fi
|
|
else
|
|
log_error "Docker group not found"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Function to test service health
|
|
test_service_health() {
|
|
log_test "Testing service health..."
|
|
|
|
cd "$PROJECT_ROOT"
|
|
|
|
local unhealthy_services=0
|
|
|
|
# Get list of services
|
|
if command -v docker-compose &> /dev/null; then
|
|
mapfile -t services < <(docker-compose -f "$COMPOSE_FILE" config --services)
|
|
else
|
|
mapfile -t services < <(docker compose -f "$COMPOSE_FILE" config --services)
|
|
fi
|
|
|
|
for service in "${services[@]}"; do
|
|
local health_status
|
|
if command -v docker-compose &> /dev/null; then
|
|
health_status=$(docker-compose -f "$COMPOSE_FILE" ps -q "$service" | xargs docker inspect --format='{{.State.Health.Status}}' 2>/dev/null || echo "none")
|
|
else
|
|
health_status=$(docker compose -f "$COMPOSE_FILE" ps -q "$service" | xargs docker inspect --format='{{.State.Health.Status}}' 2>/dev/null || echo "none")
|
|
fi
|
|
|
|
case "$health_status" in
|
|
"healthy")
|
|
log_success "Service $service is healthy"
|
|
;;
|
|
"none")
|
|
log_warning "Service $service has no health check (assuming healthy)"
|
|
;;
|
|
"unhealthy"|"starting")
|
|
log_error "Service $service is $health_status"
|
|
((unhealthy_services++))
|
|
;;
|
|
*)
|
|
log_error "Service $service has unknown status: $health_status"
|
|
((unhealthy_services++))
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ $unhealthy_services -eq 0 ]]; then
|
|
log_success "All services are healthy"
|
|
return 0
|
|
else
|
|
log_error "$unhealthy_services services are not healthy"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Function to test port accessibility
|
|
test_port_accessibility() {
|
|
log_test "Testing port accessibility..."
|
|
|
|
# shellcheck disable=SC1090,SC1091
|
|
source "$DEMO_ENV_FILE"
|
|
|
|
local ports=(
|
|
"$HOMEPAGE_PORT:Homepage"
|
|
"$DOCKER_SOCKET_PROXY_PORT:Docker Socket Proxy"
|
|
"$PIHOLE_PORT:Pi-hole"
|
|
"$DOCKHAND_PORT:Dockhand"
|
|
"$INFLUXDB_PORT:InfluxDB"
|
|
"$GRAFANA_PORT:Grafana"
|
|
"$DRAWIO_PORT:Draw.io"
|
|
"$KROKI_PORT:Kroki"
|
|
"$ATOMIC_TRACKER_PORT:Atomic Tracker"
|
|
"$ARCHIVEBOX_PORT:ArchiveBox"
|
|
"$TUBE_ARCHIVIST_PORT:Tube Archivist"
|
|
"$WAKAPI_PORT:Wakapi"
|
|
"$MAILHOG_PORT:MailHog"
|
|
"$ATUIN_PORT:Atuin"
|
|
)
|
|
|
|
local failed_ports=0
|
|
|
|
for port_info in "${ports[@]}"; do
|
|
local port="${port_info%:*}"
|
|
local service="${port_info#*:}"
|
|
|
|
if [[ -n "$port" && "$port" != " " ]]; then
|
|
if curl -f -s --max-time 5 "http://localhost:$port" >/dev/null 2>&1; then
|
|
log_success "Port $port ($service) is accessible"
|
|
else
|
|
log_error "Port $port ($service) is not accessible"
|
|
((failed_ports++))
|
|
fi
|
|
fi
|
|
done
|
|
|
|
if [[ $failed_ports -eq 0 ]]; then
|
|
log_success "All ports are accessible"
|
|
return 0
|
|
else
|
|
log_error "$failed_ports ports are not accessible"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Function to test network isolation
|
|
test_network_isolation() {
|
|
log_test "Testing network isolation..."
|
|
|
|
# shellcheck disable=SC1090,SC1091
|
|
source "$DEMO_ENV_FILE"
|
|
|
|
# Check if the network exists
|
|
if docker network ls | grep -q "$COMPOSE_NETWORK_NAME"; then
|
|
log_success "Docker network $COMPOSE_NETWORK_NAME exists"
|
|
|
|
# Check network isolation
|
|
local network_info
|
|
network_info=$(docker network inspect "$COMPOSE_NETWORK_NAME" --format='{{.Driver}}' 2>/dev/null || echo "")
|
|
|
|
if [[ "$network_info" == "bridge" ]]; then
|
|
log_success "Network is properly isolated (bridge driver)"
|
|
else
|
|
log_warning "Network driver is $network_info (expected: bridge)"
|
|
fi
|
|
|
|
return 0
|
|
else
|
|
log_error "Docker network $COMPOSE_NETWORK_NAME not found"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Function to test volume permissions
|
|
test_volume_permissions() {
|
|
log_test "Testing Docker volume permissions..."
|
|
|
|
# shellcheck disable=SC1090,SC1091
|
|
source "$DEMO_ENV_FILE"
|
|
|
|
local failed_volumes=0
|
|
|
|
# Get list of volumes for this project
|
|
local volumes
|
|
volumes=$(docker volume ls --filter "name=${COMPOSE_PROJECT_NAME}" --format "{{.Name}}" 2>/dev/null || true)
|
|
|
|
if [[ -z "$volumes" ]]; then
|
|
log_warning "No project volumes found"
|
|
return 0
|
|
fi
|
|
|
|
for volume in $volumes; do
|
|
local volume_path
|
|
local owner
|
|
volume_path=$(docker volume inspect "$volume" --format '{{ .Mountpoint }}' 2>/dev/null || echo "")
|
|
|
|
if [[ -n "$volume_path" ]]; then
|
|
owner=$(stat -c "%U:%G" "$volume_path" 2>/dev/null || echo "unknown")
|
|
|
|
if [[ "$owner" == "$(id -u):$(id -g)" || "$owner" == "root:root" ]]; then
|
|
log_success "Volume $volume has correct permissions ($owner)"
|
|
else
|
|
log_error "Volume $volume has incorrect permissions ($owner)"
|
|
((failed_volumes++))
|
|
fi
|
|
fi
|
|
done
|
|
|
|
if [[ $failed_volumes -eq 0 ]]; then
|
|
log_success "All volumes have correct permissions"
|
|
return 0
|
|
else
|
|
log_error "$failed_volumes volumes have incorrect permissions"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Function to test security compliance
|
|
test_security_compliance() {
|
|
log_test "Testing security compliance..."
|
|
|
|
# shellcheck disable=SC1090,SC1091
|
|
source "$DEMO_ENV_FILE"
|
|
|
|
local security_issues=0
|
|
|
|
# Check if Docker socket proxy is being used
|
|
cd "$PROJECT_ROOT"
|
|
|
|
if command -v docker-compose &> /dev/null; then
|
|
local socket_proxy_services
|
|
socket_proxy_services=$(docker-compose -f "$COMPOSE_FILE" config | grep -c "docker-socket-proxy" || echo "0")
|
|
else
|
|
local socket_proxy_services
|
|
socket_proxy_services=$(docker compose -f "$COMPOSE_FILE" config | grep -c "docker-socket-proxy" || echo "0")
|
|
fi
|
|
|
|
if [[ "$socket_proxy_services" -gt 0 ]]; then
|
|
log_success "Docker socket proxy service found"
|
|
else
|
|
log_error "Docker socket proxy service not found"
|
|
((security_issues++))
|
|
fi
|
|
|
|
# Check for direct Docker socket mounts (excluding docker-socket-proxy service)
|
|
local total_socket_mounts
|
|
total_socket_mounts=$(grep -c "/var/run/docker.sock" "$COMPOSE_FILE" || echo "0")
|
|
local direct_socket_mounts=$((total_socket_mounts - 1)) # Subtract 1 for the proxy service itself
|
|
|
|
if [[ "$direct_socket_mounts" -eq 0 ]]; then
|
|
log_success "No direct Docker socket mounts found"
|
|
else
|
|
log_error "Direct Docker socket mounts found ($direct_socket_mounts)"
|
|
((security_issues++))
|
|
fi
|
|
|
|
if [[ $security_issues -eq 0 ]]; then
|
|
log_success "Security compliance checks passed"
|
|
return 0
|
|
else
|
|
log_error "$security_issues security issues found"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# Function to run full test suite
|
|
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
|
|
}
|
|
|
|
# Function to run security tests only
|
|
run_security_tests() {
|
|
log_info "Running security compliance tests..."
|
|
|
|
test_file_ownership || true
|
|
test_network_isolation || true
|
|
test_security_compliance || true
|
|
|
|
display_test_results
|
|
}
|
|
|
|
# Function to run permission tests only
|
|
run_permission_tests() {
|
|
log_info "Running permission validation tests..."
|
|
|
|
test_file_ownership || true
|
|
test_user_mapping || true
|
|
test_docker_group || true
|
|
test_volume_permissions || true
|
|
|
|
display_test_results
|
|
}
|
|
|
|
# Function to run network tests only
|
|
run_network_tests() {
|
|
log_info "Running network isolation tests..."
|
|
|
|
test_network_isolation || true
|
|
test_port_accessibility || true
|
|
|
|
display_test_results
|
|
}
|
|
|
|
# Function to display test results
|
|
display_test_results() {
|
|
echo ""
|
|
echo "===================================="
|
|
echo "🧪 TEST RESULTS SUMMARY"
|
|
echo "===================================="
|
|
echo "Total Tests: $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
|
|
}
|
|
|
|
# Function to show usage
|
|
show_usage() {
|
|
echo "Usage: $0 {full|security|permissions|network|help}"
|
|
echo ""
|
|
echo "Test Categories:"
|
|
echo " full - Run comprehensive test suite"
|
|
echo " security - Run security compliance tests only"
|
|
echo " permissions - Run permission validation tests only"
|
|
echo " network - Run network isolation tests only"
|
|
echo " help - Show this help message"
|
|
}
|
|
|
|
# Main script execution
|
|
main() {
|
|
case "${1:-full}" in
|
|
full)
|
|
run_full_tests
|
|
;;
|
|
security)
|
|
run_security_tests
|
|
;;
|
|
permissions)
|
|
run_permission_tests
|
|
;;
|
|
network)
|
|
run_network_tests
|
|
;;
|
|
help|--help|-h)
|
|
show_usage
|
|
;;
|
|
*)
|
|
log_error "Unknown test category: $1"
|
|
show_usage
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Execute main function with all arguments
|
|
main "$@" |