#!/bin/bash # ============================================================================= # TSYS Developer Support Stack - Demo Testing & Validation Script # ============================================================================= # # This script performs comprehensive QA, security compliance, and validation # of demo stack deployment using Docker containers only. # # Usage: ./demo-test.sh [full|security|permissions|network|health] # ============================================================================= set -euo pipefail # ============================================================================= # CONFIGURATION # ============================================================================= # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Script directory SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" # Test counters TOTAL_TESTS=0 PASSED_TESTS=0 FAILED_TESTS=0 # ============================================================================= # UTILITY FUNCTIONS # ============================================================================= print_header() { echo -e "\n${BLUE}============================================================================${NC}" echo -e "${BLUE}$1${NC}" echo -e "${BLUE}============================================================================${NC}" } print_success() { echo -e "${GREEN}✅ $1${NC}" ((PASSED_TESTS++)) } print_error() { echo -e "${RED}❌ $1${NC}" ((FAILED_TESTS++)) } print_warning() { echo -e "${YELLOW}⚠️ $1${NC}" } print_info() { echo -e "${BLUE}ℹ️ $1${NC}" } test_result() { local condition="$1" local description="$2" ((TOTAL_TESTS++)) if eval "$condition"; then print_success "$description" else print_error "$description" fi } # ============================================================================= # DOCKER-BASED QA FUNCTIONS # ============================================================================= run_shellcheck() { print_header "🐚 SHELLCHECK VALIDATION" local shellcheck_failed=0 for script in demo-stack.sh demo-test.sh; do if [[ -f "$script" ]]; then print_info "Checking $script with ShellCheck..." if docker run --rm \ -v "$(pwd):/workdir" \ -w /workdir \ koalaman/shellcheck:stable \ --severity=warning \ "$script"; then print_success "$script passed ShellCheck validation" else print_error "$script failed ShellCheck validation" shellcheck_failed=1 fi else print_warning "$script not found" fi done return $shellcheck_failed } run_yamllint() { print_header "📄 YAML VALIDATION" local yamllint_failed=0 if [[ -f "docker-compose.yml.template" ]]; then print_info "Checking docker-compose.yml.template with YAMLLint..." # Create a minimal yamllint config cat > .yamllint.yml << 'EOF' --- extends: default rules: line-length: max: 120 comments: min-spaces-from-content: 1 EOF if docker run --rm \ -v "$(pwd):/workdir" \ -w /workdir \ cytopia/yamllint:latest \ -c .yamllint.yml \ docker-compose.yml.template; then print_success "YAML files passed YAMLLint validation" else print_error "YAML files failed YAMLLint validation" yamllint_failed=1 fi # Clean up config rm -f .yamllint.yml else print_warning "docker-compose.yml.template not found" yamllint_failed=1 fi return $yamllint_failed } run_proselint() { print_header "📝 PROSELINT VALIDATION" local proselint_failed=0 for doc_file in PRD.md README.md AGENTS.md; do if [[ -f "$doc_file" ]]; then print_info "Checking $doc_file with Proselint..." # Create temporary proselint config to ignore false positives cat > .proselint-config.json << 'EOF' { "flags": [ "typography.symbols.curly_quotes", "leonard.exclamation.30ppm" ] } EOF proselint_output=$(docker run --rm \ -v "$(pwd):/workdir" \ -w /workdir \ ghcr.io/pycqa/proselint:latest \ --config .proselint-config.json \ "$doc_file" 2>/dev/null || true) # Clean up config rm -f .proselint-config.json if [[ -z "$proselint_output" ]]; then print_success "$doc_file passed Proselint validation" else print_warning "$doc_file has prose issues:" echo "$proselint_output" | head -10 proselint_failed=1 fi else print_warning "$doc_file not found" fi done return $proselint_failed } run_vale() { print_header "📖 VALE VALIDATION" local vale_failed=0 # Create Vale config cat > .vale.ini << EOF [*.md] BasedOnStyles = Vale Vocab = TSYS [TSYS] Terms = TSYS, Docker, Kubernetes, demo IgnoreCase = true EOF for doc_file in PRD.md README.md AGENTS.md; do if [[ -f "$doc_file" ]]; then print_info "Checking $doc_file with Vale..." vale_output=$(docker run --rm \ -v "$(pwd):/workdir" \ -w /workdir \ jdkato/vale:latest \ --minAlertLevel=error \ --config=.vale.ini \ "$doc_file" 2>/dev/null || true) if [[ -z "$vale_output" ]]; then print_success "$doc_file passed Vale validation" else print_warning "$doc_file has Vale issues:" echo "$vale_output" | head -10 vale_failed=1 fi else print_warning "$doc_file not found" fi done # Clean up config rm -f .vale.ini return $vale_failed } run_hadolint() { print_header "🐳 DOCKERFILE VALIDATION" local hadolint_failed=0 # Check if we have any Dockerfiles (exclude toolchain files) while IFS= read -r -d '' dockerfile; do print_info "Checking $dockerfile with Hadolint..." if docker run --rm \ -v "$(pwd):/workdir" \ -w /workdir \ hadolint/hadolint:latest-alpine \ "$dockerfile"; then print_success "$dockerfile passed Hadolint validation" else print_error "$dockerfile failed Hadolint validation" hadolint_failed=1 fi done < <(find . -name "Dockerfile*" -type f ! -name "Dockerfile.*" -print0 2>/dev/null) if ! find . -name "Dockerfile*" -type f ! -name "Dockerfile.*" -print0 2>/dev/null | grep -qz .; then print_info "No Dockerfiles found to validate" fi return $hadolint_failed } check_image_versions() { print_header "🏷️ IMAGE VERSION VALIDATION" local version_failed=0 print_info "Checking for 'latest' tags in docker-compose.yml.template..." if grep -q ":latest" docker-compose.yml.template; then print_error "Found 'latest' tags in docker-compose.yml.template:" grep -n ":latest" docker-compose.yml.template version_failed=1 else print_success "No 'latest' tags found in docker-compose.yml.template" fi return $version_failed } check_file_permissions() { print_header "🔐 FILE PERMISSIONS VALIDATION" local permission_failed=0 # Check script permissions if [[ -f "demo-stack.sh" ]]; then if [[ -x "demo-stack.sh" ]]; then print_success "demo-stack.sh is executable" else print_error "demo-stack.sh is not executable" permission_failed=1 fi fi if [[ -f "demo-test.sh" ]]; then if [[ -x "demo-test.sh" ]]; then print_success "demo-test.sh is executable" else print_error "demo-test.sh is not executable" permission_failed=1 fi fi # Check for world-writable files local world_writable world_writable=$(find . -type f -perm -002 2>/dev/null | wc -l) if [[ "$world_writable" -eq 0 ]]; then print_success "No world-writable files found" else print_error "Found $world_writable world-writable files" permission_failed=1 fi return $permission_failed } validate_environment() { print_header "🌍 ENVIRONMENT VALIDATION" local env_failed=0 # Load environment variables # shellcheck source=demo.env if [[ -f "demo.env" ]]; then set -a source demo.env set +a fi # Check if demo.env exists if [[ -f "demo.env" ]]; then print_success "demo.env exists" else print_error "demo.env not found" env_failed=1 fi # Check if docker-compose.yml.template exists if [[ -f "docker-compose.yml.template" ]]; then print_success "docker-compose.yml.template exists" else print_error "docker-compose.yml.template not found" env_failed=1 fi # Check if required scripts exist for script in demo-stack.sh demo-test.sh; do if [[ -f "$script" ]]; then print_success "$script exists" else print_error "$script not found" env_failed=1 fi done return $env_failed } # ============================================================================= # SECURITY VALIDATION FUNCTIONS # ============================================================================= validate_user_mapping() { print_header "👤 USER MAPPING VALIDATION" # Get current user info current_uid=$(id -u) local current_uid current_gid=$(id -g) local current_gid current_user=$(id -un) local current_user print_info "Current user: $current_user (UID: $current_uid, GID: $current_gid)" # Check for root-owned files in project directory root_files=$(find . -user root 2>/dev/null | wc -l) local root_files test_result "[[ $root_files -eq 0 ]]" "No root-owned files in project directory" # Verify demo scripts use current user if [[ -f "demo-stack.sh" ]]; then test_result "[[ -r \"demo-stack.sh\" ]]" "demo-stack.sh readable by current user" fi # Check docker group access user_groups=$(id -Gn 2>/dev/null | tr ' ' '\n' | grep -E '^docker$' || echo "") local user_groups test_result "[[ -n \"$user_groups\" ]]" "Current user in docker group" } validate_docker_socket_security() { print_header "🔒 DOCKER SOCKET SECURITY VALIDATION" # Check if docker-socket-proxy is running proxy_running=$(docker compose ps -q docker-socket-proxy 2>/dev/null) local proxy_running test_result "[[ -n \"$proxy_running\" ]]" "Docker socket proxy running" if [[ -n "$proxy_running" ]]; then # Check if proxy container has proper restrictions proxy_container="${COMPOSE_PROJECT_NAME}-docker-socket-proxy" test_result=$(docker exec "$proxy_container" curl -s -o /dev/null -w "%{http_code}" http://localhost:2375/containers/json 2>/dev/null || echo "000") local test_result test_result "[[ \"$test_result\" == \"403\" ]]" "Docker socket proxy security restrictions" # Check if any service has direct docker socket access exposed_socket=$(docker compose ps --format "{{.Ports}}" portainer 2>/dev/null | grep -o "/var/run/docker.sock" || echo "") local exposed_socket test_result "[[ -z \"$exposed_socket\" ]]" "Docker socket not directly exposed" fi } validate_network_isolation() { print_header "🌐 NETWORK ISOLATION VALIDATION" # Check if demo network exists network_exists=$(docker network ls -q -f name="${COMPOSE_NETWORK_NAME}" 2>/dev/null) local network_exists test_result "[[ -n \"$network_exists\" ]]" "Demo network exists" if [[ -n "$network_exists" ]]; then # Check network driver network_driver=$(docker network inspect "${COMPOSE_NETWORK_NAME}" -f '{{.Driver}}' 2>/dev/null) local network_driver test_result "[[ \"$network_driver\" == \"bridge\" ]]" "Network isolation (bridge driver)" fi } # ============================================================================= # HEALTH CHECK FUNCTIONS # ============================================================================= check_service_health() { local service_name="$1" local url="$2" print_info "Checking $service_name health..." http_code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 "$url" 2>/dev/null || echo "000") local http_code if [[ "$http_code" =~ ^[23] ]]; then print_success "$service_name is healthy (HTTP $http_code)" return 0 else print_error "$service_name is unhealthy (HTTP $http_code)" return 1 fi } validate_service_health() { print_header "🏥 SERVICE HEALTH VALIDATION" local health_failed=0 # Load environment variables # shellcheck source=demo.env if [[ -f "demo.env" ]]; then set -a source demo.env set +a fi # Check core services if check_service_health "Homepage" "http://localhost:${HOMEPAGE_PORT}/"; then : # Homepage is healthy else health_failed=1 fi # Check other services if ports are defined if [[ -n "${GRAFANA_PORT:-}" ]]; then check_service_health "Grafana" "http://localhost:${GRAFANA_PORT}/" || health_failed=1 fi if [[ -n "${PORTAINER_PORT:-}" ]]; then check_service_health "Portainer" "http://localhost:${PORTAINER_PORT}/" || health_failed=1 fi return $health_failed } # ============================================================================= # DEMO CONFIGURATION VALIDATION # ============================================================================= validate_demo_configuration() { print_header "🎯 DEMO CONFIGURATION VALIDATION" # Load environment variables # shellcheck source=demo.env if [[ -f "demo.env" ]]; then set -a source demo.env set +a fi # Check demo credentials test_result "[[ \"$GRAFANA_ADMIN_PASSWORD\" == \"demo_password\" ]]" "Grafana demo credentials" test_result "[[ \"$ATOMIC_TRACKER_USE_DUMMY_DATA\" == \"1\" ]]" "Atomic Tracker demo configuration" # Check project naming test_result "[[ \"$COMPOSE_PROJECT_NAME\" == \"tsysdevstack-supportstack-demo\" ]]" "Project naming convention" # Check port ranges if [[ -n "${HOMEPAGE_PORT:-}" ]]; then test_result "[[ $HOMEPAGE_PORT -ge 4000 && $HOMEPAGE_PORT -le 4099 ]]" "Homepage port in allowed range (4000-4099)" fi } # ============================================================================= # PERFORMANCE VALIDATION # ============================================================================= validate_performance() { print_header "📊 PERFORMANCE VALIDATION" # Check resource usage print_info "Checking resource usage..." # Get memory usage memory_usage=$(docker stats --no-stream --format "table {{.Container}}\t{{.MemUsage}}" 2>/dev/null | grep -E "(homepage|pihole|portainer|influxdb|grafana)" | awk '{sum+=$2} END {print sum}' || echo "0") local memory_usage # Get container count container_count=$(docker compose ps -q 2>/dev/null | wc -l) local container_count print_info "Memory usage: ${memory_usage}B" print_info "Container count: $container_count" # Performance thresholds test_result "[[ $container_count -le 10 ]]" "Container count within limits (≤10)" test_result "[[ ${memory_usage%.*} -le 1048576 ]]" "Memory usage within limits (≤1GB)" } # ============================================================================= # MAIN EXECUTION # ============================================================================= show_usage() { echo "Usage: $0 [full|security|permissions|network|health|qa]" echo "" echo "Options:" echo " full - Run all validations" echo " security - Security validation only" echo " permissions- File permissions validation only" echo " network - Network isolation validation only" echo " health - Service health checks only" echo " qa - QA tools validation only" echo "" echo "Examples:" echo " $0 full # Run complete validation" echo " $0 security # Security checks only" echo " $0 qa # QA tools only" } run_qa_validation() { print_header "🔍 COMPREHENSIVE QA VALIDATION" print_info "Running all QA checks using Docker containers only..." local overall_failed=0 # Run all QA validations validate_environment || overall_failed=1 run_shellcheck || overall_failed=1 run_yamllint || overall_failed=1 run_proselint || overall_failed=1 run_vale || overall_failed=1 run_hadolint || overall_failed=1 check_image_versions || overall_failed=1 check_file_permissions || overall_failed=1 # Final result print_header "📋 QA SUMMARY" if [[ $overall_failed -eq 0 ]]; then print_success "All QA checks passed! ✨" echo -e "\n${GREEN}The project is ready for deployment.${NC}" else print_error "Some QA checks failed. Please fix issues above." echo -e "\n${RED}The project is not ready for deployment.${NC}" fi return $overall_failed } main() { case "${1:-full}" in "full") print_header "🚀 COMPREHENSIVE DEMO STACK VALIDATION" validate_environment run_qa_validation validate_user_mapping validate_docker_socket_security validate_network_isolation validate_service_health validate_demo_configuration validate_performance print_header "📋 FINAL SUMMARY" echo -e "${BLUE}Total Tests:${NC} $TOTAL_TESTS" echo -e "${GREEN}Passed:${NC} $PASSED_TESTS" echo -e "${RED}Failed:${NC} $FAILED_TESTS" if [[ $FAILED_TESTS -eq 0 ]]; then echo -e "\n${GREEN}🎉 All validations passed! The demo stack is ready.${NC}" exit 0 else echo -e "\n${RED}❌ Some validations failed. Please review the issues above.${NC}" exit 1 fi ;; "security") validate_user_mapping validate_docker_socket_security validate_network_isolation ;; "permissions") validate_user_mapping check_file_permissions ;; "network") validate_network_isolation ;; "health") validate_service_health ;; "qa") run_qa_validation ;; "help"|"-h"|"--help") show_usage exit 0 ;; *) echo -e "${RED}Error: Unknown option '$1'${NC}" echo "" show_usage exit 1 ;; esac } # Run main function with all arguments main "$@"