Files
digiserver-v2/verify-deployment.sh
T
ske087 46602f1933 Sanitize codebase, reorganize docs, and add missing deploy files
Remove dead code identified in docs/SANITIZATION-REVIEW.md:
- app/blueprints/content_old.py and app/blueprints/playlist.py
- app/models/group.py, app/utils/nginx_config_reader.py
- orphaned templates (content_list, edit_content, upload_content,
  player_page) and the related group/Template references

Result: 6 blueprints, 82 routes, no dead modules or orphan templates.

Add files that deploy.sh and docker-entrypoint.sh already require but
which were never tracked:
- https_manager.py       (referenced by deploy.sh, migrate_network.sh,
                          docker-entrypoint.sh)
- Caddyfile.example      (seeded by deploy.sh; its absence aborts deploy)

Relocate generated Graphify artifacts from graphify-out/ to
docs/graphify-out/ (110 files, no content change) and archive the
superseded docs under docs/.

Ignore hygiene:
- ignore ad-hoc .env backups (.env.bak*) — they contain live secrets
- keep the pre-sanitization snapshots (docs/legacy code/,
  docs/old_code_documentation/) on disk but out of the repo

Fix .env.example: drop a duplicated config block, genericize the
hardcoded host IP, and document HOSTNAME_INTERNAL.
2026-09-11 12:18:34 +03:00

429 lines
15 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/bash
# Production Deployment Verification Script
# Run this before and after production deployment
set -e
echo "╔════════════════════════════════════════════════════════════════╗"
echo "║ DigiServer v2 Production Deployment Verification ║"
echo "╚════════════════════════════════════════════════════════════════╝"
TIMESTAMP=$(date +%Y-%m-%d\ %H:%M:%S)
cd "$(dirname "$0")"
# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Counters
PASSED=0
FAILED=0
WARNINGS=0
# Helper functions
# NOTE: `((VAR++))` evaluates to the PRE-increment value, so the very first
# increment returns 0 — a non-zero exit status that `set -e` treats as a fatal
# error, aborting the whole script after the first check. Use `VAR=$((VAR+1))`
# (always status 0) instead.
pass() {
echo -e "${GREEN}${NC} $1"
PASSED=$((PASSED + 1))
}
fail() {
echo -e "${RED}${NC} $1"
FAILED=$((FAILED + 1))
}
warn() {
echo -e "${YELLOW}${NC} $1"
WARNINGS=$((WARNINGS + 1))
}
info() {
echo -e "${BLUE}${NC} $1"
}
section() {
echo -e "\n${BLUE}═══════════════════════════════════════${NC}"
echo -e "${BLUE} $1${NC}"
echo -e "${BLUE}═══════════════════════════════════════${NC}"
}
# ============================================================================
section "1. Git Status"
# ============================================================================
if git rev-parse --git-dir > /dev/null 2>&1; then
pass "Git repository initialized"
BRANCH=$(git rev-parse --abbrev-ref HEAD)
COMMIT=$(git rev-parse --short HEAD)
info "Current branch: $BRANCH, Commit: $COMMIT"
if [ -n "$(git status --porcelain)" ]; then
warn "Uncommitted changes detected"
git status --short
else
pass "All changes committed"
fi
else
fail "Not a git repository"
fi
# ============================================================================
section "2. Environment Configuration"
# ============================================================================
if [ -f .env ]; then
pass ".env file exists"
else
warn ".env file not found (using defaults or docker-compose environment)"
fi
if [ -f .env.example ]; then
pass ".env.example template exists"
else
warn ".env.example template missing"
fi
# ============================================================================
section "3. Docker Configuration"
# ============================================================================
if command -v docker &> /dev/null; then
pass "Docker installed"
DOCKER_VERSION=$(docker --version | cut -d' ' -f3 | tr -d ',')
info "Docker version: $DOCKER_VERSION"
else
fail "Docker not installed"
fi
# Accept either the modern compose plugin or the standalone v1 binary.
if docker compose version &> /dev/null; then
COMPOSE="docker compose"
pass "Docker Compose plugin installed"
DC_VERSION=$(docker compose version --short 2>/dev/null || docker compose version | head -1)
info "Compose version: $DC_VERSION"
elif command -v docker-compose &> /dev/null; then
COMPOSE="docker-compose"
warn "Using legacy 'docker-compose' (v1); the 'docker compose' plugin is unavailable"
DC_VERSION=$(docker-compose --version | cut -d' ' -f3 | tr -d ',')
info "Docker Compose version: $DC_VERSION"
# Compose v1 builds require buildx >= 0.17; older buildx must use `docker build`.
BUILDX_VER=$(docker buildx version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+' | head -1 || true)
if [ -n "$BUILDX_VER" ]; then
_maj="${BUILDX_VER%%.*}"; _min="${BUILDX_VER##*.}"
if [ "$_maj" -eq 0 ] && [ "$_min" -lt 17 ]; then
info "buildx $BUILDX_VER too old for compose v1 builds — deploy.sh falls back to 'docker build'"
fi
else
info "buildx not available — deploy.sh falls back to 'docker build'"
fi
else
fail "Docker Compose not installed"
COMPOSE=""
fi
if [ -f docker-compose.yml ]; then
pass "docker-compose.yml exists"
# Validate syntax
if [ -n "$COMPOSE" ] && $COMPOSE config > /dev/null 2>&1; then
pass "docker-compose.yml syntax valid"
else
fail "docker-compose.yml syntax error"
fi
else
fail "docker-compose.yml not found"
fi
# ============================================================================
section "4. Dockerfile & Images"
# ============================================================================
if [ -f Dockerfile ]; then
pass "Dockerfile exists"
# Check for security best practices
if grep -q "HEALTHCHECK" Dockerfile; then
pass "Health check configured"
else
warn "No health check in Dockerfile"
fi
if grep -q "USER appuser" Dockerfile || grep -q "USER.*:1000" Dockerfile; then
pass "Non-root user configured"
else
warn "Root user may be used in container"
fi
if grep -q "FROM.*alpine\|FROM.*slim\|FROM.*distroless" Dockerfile; then
pass "Minimal base image used"
else
warn "Large base image detected"
fi
else
fail "Dockerfile not found"
fi
# ============================================================================
section "5. Python Dependencies"
# ============================================================================
if [ -f requirements.txt ]; then
pass "requirements.txt exists"
PACKAGE_COUNT=$(wc -l < requirements.txt)
info "Total packages: $PACKAGE_COUNT"
# Check for critical packages
for pkg in Flask SQLAlchemy gunicorn flask-cors cryptography; do
if grep -q "$pkg" requirements.txt; then
pass "$pkg installed"
else
warn "$pkg not found in requirements.txt"
fi
done
# Check for specific versions
FLASK_VERSION=$(grep "^Flask==" requirements.txt 2>/dev/null | cut -d'=' -f3 || true)
SQLALCHEMY_VERSION=$(grep "^SQLAlchemy==" requirements.txt | cut -d'=' -f3)
if [ -n "$FLASK_VERSION" ]; then
info "Flask version: $FLASK_VERSION"
fi
if [ -n "$SQLALCHEMY_VERSION" ]; then
info "SQLAlchemy version: $SQLALCHEMY_VERSION"
fi
else
fail "requirements.txt not found"
fi
# ============================================================================
section "6. Database Configuration"
# ============================================================================
if [ -d migrations ]; then
pass "migrations directory exists"
MIGRATION_COUNT=$(find migrations -name "*.py" | wc -l)
info "Migration files: $MIGRATION_COUNT"
if [ "$MIGRATION_COUNT" -gt 0 ]; then
pass "Database migrations configured"
else
warn "No migration files found"
fi
else
warn "migrations directory not found"
fi
# ============================================================================
section "7. TLS Certificate (Caddy)"
# ============================================================================
# Caddy stores its internal CA and issued certificates under data/caddy-data.
# Those files are created by the (root) Caddy process, so read them through the
# container rather than from the host filesystem.
CADDY_CA_IN_CONTAINER="/data/caddy/pki/authorities/local/root.crt"
if $COMPOSE exec -T caddy sh -c "test -f $CADDY_CA_IN_CONTAINER" 2>/dev/null; then
pass "Caddy internal CA root certificate found"
CERT_EXPIRY=$($COMPOSE exec -T caddy sh -c \
"caddy version >/dev/null 2>&1; cat $CADDY_CA_IN_CONTAINER" 2>/dev/null \
| openssl x509 -enddate -noout 2>/dev/null | cut -d= -f2)
if [ -n "$CERT_EXPIRY" ]; then
EXPIRY_EPOCH=$(date -d "$CERT_EXPIRY" +%s 2>/dev/null || echo 0)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))
info "Root CA expires: $CERT_EXPIRY"
info "Days remaining: $DAYS_LEFT days"
if [ "$DAYS_LEFT" -lt 0 ]; then
fail "Caddy internal CA has expired!"
elif [ "$DAYS_LEFT" -lt 30 ]; then
warn "Caddy internal CA expires in less than 30 days"
else
pass "Caddy internal CA is valid"
fi
fi
info "Install this CA on client devices to trust the internal certificate:"
info " $COMPOSE cp caddy:$CADDY_CA_IN_CONTAINER ./caddy-root.crt"
else
info "No Caddy internal CA yet (created on first 'tls internal' issuance)"
fi
# ============================================================================
section "8. Configuration Files"
# ============================================================================
if [ -f app/config.py ]; then
pass "Flask config.py exists"
if grep -q "class ProductionConfig" app/config.py; then
pass "ProductionConfig class defined"
else
warn "ProductionConfig class missing"
fi
if grep -q "SESSION_COOKIE_SECURE" app/config.py; then
pass "SESSION_COOKIE_SECURE configured"
else
warn "SESSION_COOKIE_SECURE not configured"
fi
else
fail "app/config.py not found"
fi
# The reverse proxy is Caddy; data/Caddyfile is the live config.
if [ -f data/Caddyfile ]; then
pass "data/Caddyfile exists"
if grep -q "reverse_proxy" data/Caddyfile; then
pass "Caddy reverse_proxy configured"
else
warn "No reverse_proxy directive in Caddyfile"
fi
if grep -q "admin " data/Caddyfile; then
pass "Caddy admin API configured (needed for live reloads)"
else
warn "Caddy admin API not configured — HTTPS changes cannot hot-reload"
fi
if grep -qE "tls internal" data/Caddyfile; then
info "Using Caddy internal CA (intranet/non-public hostname)"
elif grep -qE "^https://|^[a-zA-Z0-9.-]+ \{" data/Caddyfile; then
info "TLS enabled (Let's Encrypt)"
else
info "HTTP-only configuration"
fi
elif [ -f Caddyfile.example ]; then
info "data/Caddyfile not present yet; deploy.sh seeds it from Caddyfile.example"
else
fail "Neither data/Caddyfile nor Caddyfile.example found"
fi
# ============================================================================
section "9. Runtime Verification"
# ============================================================================
if [ -n "$COMPOSE" ] && $COMPOSE ps 2>/dev/null | grep -q "Up"; then
pass "Docker containers are running"
if $COMPOSE ps 2>/dev/null | grep -q "digiserver-v2.*healthy"; then
pass "DigiServer app container is healthy"
else
warn "DigiServer app container health status unknown"
fi
if $COMPOSE ps 2>/dev/null | grep -q "digiserver-caddy.*healthy"; then
pass "Caddy container is healthy"
else
warn "Caddy container health status unknown"
fi
# Probe the actual endpoints rather than trusting status alone.
if command -v curl >/dev/null 2>&1; then
HTTP_PORT="${HTTP_PORT:-80}"
HTTPS_PORT="${HTTPS_PORT:-443}"
HTTP_URL="http://localhost"
[ "$HTTP_PORT" != "80" ] && HTTP_URL="http://localhost:$HTTP_PORT"
HTTPS_URL="https://localhost"
[ "$HTTPS_PORT" != "443" ] && HTTPS_URL="https://localhost:$HTTPS_PORT"
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -m 5 "$HTTP_URL/" 2>/dev/null || echo 000)
if [ "$HTTP_CODE" != "000" ]; then
pass "HTTP endpoint responds ($HTTP_URL, status $HTTP_CODE)"
else
warn "HTTP endpoint not responding ($HTTP_URL)"
fi
# TLS site blocks match on the requested name (SNI) or on default_sni.
# localhost matches neither when the server is configured for an IP /
# intranet hostname, so probe the address the config actually serves.
# NOTE: default_sni is indented inside the global block, so anchor on
# optional leading whitespace, not `^`.
PROBE_HOST=$(awk '/^[[:space:]]*default_sni/{print $2; exit}' data/Caddyfile 2>/dev/null || true)
if [ -z "$PROBE_HOST" ]; then
PROBE_HOST=$(awk '/^[[:space:]]*https:\/\//{sub(/^[[:space:]]*https:\/\//,""); sub(/[[:space:]{].*$/,""); print; exit}' data/Caddyfile 2>/dev/null || true)
fi
if [ -z "$PROBE_HOST" ]; then
PROBE_HOST="localhost"
fi
if curl -sk -o /dev/null -m 5 --resolve "$PROBE_HOST:$HTTPS_PORT:127.0.0.1" \
"https://$PROBE_HOST:$HTTPS_PORT/" 2>/dev/null; then
pass "HTTPS endpoint responds (https://$PROBE_HOST:$HTTPS_PORT)"
elif grep -qE "tls internal|^[[:space:]]*https://" data/Caddyfile 2>/dev/null; then
warn "HTTPS configured but not responding on https://$PROBE_HOST:$HTTPS_PORT"
else
info "HTTPS not configured (HTTP-only deployment)"
fi
fi
else
info "Docker containers not running (will start on deployment)"
fi
# ============================================================================
section "10. Security Best Practices"
# ============================================================================
# Check for hardcoded secrets
if grep -r "SECRET_KEY\|PASSWORD\|API_KEY" app/ 2>/dev/null | grep -v "os.getenv\|config.py\|#" | wc -l | grep -q "^0$"; then
pass "No hardcoded secrets found"
else
warn "Possible hardcoded secrets detected (verify they use os.getenv)"
fi
# Check for debug mode. Only ProductionConfig matters — config.py intentionally
# sets DEBUG=True for DevelopmentConfig and TestingConfig.
# NOTE: grep exits 1 when it matches nothing; under `set -e` that would abort
# the script, so the whole pipeline must end with a command that always succeeds.
DEBUG_LINE=$(awk '/class ProductionConfig/,/^class |^# Configuration/' app/config.py 2>/dev/null \
| grep -E "^[[:space:]]*DEBUG[[:space:]]*=[[:space:]]*True" || true)
if [ -n "$DEBUG_LINE" ]; then
fail "DEBUG mode is enabled in ProductionConfig"
elif grep -q "class ProductionConfig" app/config.py 2>/dev/null; then
pass "Debug mode disabled in ProductionConfig"
else
warn "ProductionConfig class not found — cannot verify debug mode"
fi
# ============================================================================
section "Summary"
# ============================================================================
echo ""
echo -e "Test Results:"
echo -e " ${GREEN}Passed: $PASSED${NC}"
echo -e " ${YELLOW}Warnings: $WARNINGS${NC}"
echo -e " ${RED}Failed: $FAILED${NC}"
TOTAL=$((PASSED + FAILED + WARNINGS))
PERCENTAGE=$((PASSED * 100 / (PASSED + FAILED)))
if [ "$FAILED" -eq 0 ]; then
echo -e "\n${GREEN}✓ Production Deployment Ready!${NC}"
echo "Recommendation: Safe to deploy to production"
exit 0
elif [ "$FAILED" -le 2 ] && [ "$WARNINGS" -gt 0 ]; then
echo -e "\n${YELLOW}⚠ Deployment Possible with Caution${NC}"
echo "Recommendation: Address warnings before deployment"
exit 0
else
echo -e "\n${RED}✗ Deployment Not Recommended${NC}"
echo "Recommendation: Fix critical failures before deployment"
exit 1
fi