Files
ivatar/scripts/check_deployment.py

449 lines
15 KiB
Python
Executable File

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Libravatar Deployment Verification Script
This script verifies that Libravatar deployments are working correctly by:
- Checking version endpoint
- Testing avatar functionality with various sizes
- Verifying stats endpoint
- Testing redirect behavior
Usage:
python3 check_deployment.py --dev # Test dev deployment
python3 check_deployment.py --prod # Test production deployment
python3 check_deployment.py --endpoint <url> # Test custom endpoint
python3 check_deployment.py --dev --prod # Test both deployments
"""
import argparse
import json
import random
import ssl
import sys
import tempfile
import time
from typing import Dict, Optional, Tuple
from urllib.parse import urljoin
from urllib.request import urlopen, Request
from urllib.error import HTTPError, URLError
try:
from PIL import Image
PIL_AVAILABLE = True
except ImportError:
PIL_AVAILABLE = False
# Configuration
DEV_URL = "https://dev.libravatar.org"
PROD_URL = "https://libravatar.org"
MAX_RETRIES = 5
RETRY_DELAY = 10
# ANSI color codes
class Colors:
RED = "\033[0;31m"
GREEN = "\033[0;32m"
YELLOW = "\033[1;33m"
BLUE = "\033[0;34m"
NC = "\033[0m" # No Color
def colored_print(message: str, color: str = Colors.NC) -> None:
"""Print a colored message."""
print(f"{color}{message}{Colors.NC}")
def make_request(
url: str,
method: str = "GET",
headers: Optional[Dict[str, str]] = None,
follow_redirects: bool = True,
binary: bool = False,
) -> Tuple[bool, Optional[bytes], Optional[Dict[str, str]]]:
"""
Make an HTTP request and return success status, content, and headers.
Args:
url: URL to request
method: HTTP method
headers: Additional headers
follow_redirects: Whether to follow redirects automatically
Returns:
Tuple of (success, content, headers)
"""
req = Request(url, headers=headers or {})
req.get_method = lambda: method
# Create SSL context that handles certificate verification issues
ssl_context = ssl.create_default_context()
# Try with SSL verification first
try:
opener = urlopen
if not follow_redirects:
# Create a custom opener that doesn't follow redirects
import urllib.request
class NoRedirectHandler(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
opener = urllib.request.build_opener(NoRedirectHandler)
if follow_redirects:
with opener(req, context=ssl_context) as response:
content = response.read()
if not binary:
try:
content = content.decode("utf-8")
except UnicodeDecodeError:
# If content is not text (e.g., binary image), return empty string
content = ""
headers = dict(response.headers)
return True, content, headers
else:
response = opener.open(req)
content = response.read()
if not binary:
try:
content = content.decode("utf-8")
except UnicodeDecodeError:
content = ""
headers = dict(response.headers)
return True, content, headers
except URLError as url_err:
# Check if this is an SSL error wrapped in URLError
if isinstance(url_err.reason, ssl.SSLError):
# If SSL fails, try with unverified context (less secure but works for testing)
ssl_context_unverified = ssl.create_default_context()
ssl_context_unverified.check_hostname = False
ssl_context_unverified.verify_mode = ssl.CERT_NONE
try:
if follow_redirects:
with urlopen(req, context=ssl_context_unverified) as response:
content = response.read()
if not binary:
try:
content = content.decode("utf-8")
except UnicodeDecodeError:
content = ""
headers = dict(response.headers)
return True, content, headers
else:
import urllib.request
class NoRedirectHandler(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
opener = urllib.request.build_opener(NoRedirectHandler)
response = opener.open(req)
content = response.read()
if not binary:
try:
content = content.decode("utf-8")
except UnicodeDecodeError:
content = ""
headers = dict(response.headers)
return True, content, headers
except Exception:
return False, None, None
else:
return False, None, None
except HTTPError:
return False, None, None
def check_version_endpoint(base_url: str) -> Tuple[bool, Optional[Dict]]:
"""Check the version endpoint and return deployment info."""
version_url = urljoin(base_url, "/deployment/version/")
success, content, _ = make_request(version_url)
if not success or not content:
return False, None
try:
version_info = json.loads(content)
return True, version_info
except json.JSONDecodeError:
return False, None
def test_avatar_redirect(base_url: str) -> bool:
"""Test that invalid avatar requests redirect to default image."""
avatar_url = urljoin(base_url, "/avatar/test@example.com")
# Use a simple approach: check if the final URL after redirect contains deadbeef.png
try:
req = Request(avatar_url)
ssl_context = ssl.create_default_context()
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
with urlopen(req, context=ssl_context) as response:
final_url = response.geturl()
return "deadbeef.png" in final_url
except Exception:
return False
def test_avatar_sizing(base_url: str) -> bool:
"""Test avatar endpoint with random sizes."""
# Use a known test hash for consistent testing
test_hash = "63a75a80e6b1f4adfdb04c1ca02e596c"
# Generate random sizes between 50-250
sizes = [random.randint(50, 250) for _ in range(2)]
for size in sizes:
avatar_url = urljoin(base_url, f"/avatar/{test_hash}?s={size}")
# Download image to temporary file
success, content, _ = make_request(avatar_url, binary=True)
if not success or not content:
colored_print(f"❌ Avatar endpoint failed for size {size}", Colors.RED)
return False
# Check image dimensions
if PIL_AVAILABLE:
try:
with tempfile.NamedTemporaryFile(suffix=".jpg") as temp_file:
temp_file.write(content)
temp_file.flush()
with Image.open(temp_file.name) as img:
width, height = img.size
if width == size and height == size:
colored_print(
f"✅ Avatar size {size}x{size} verified", Colors.GREEN
)
else:
colored_print(
f"❌ Avatar wrong size: expected {size}x{size}, got {width}x{height}",
Colors.RED,
)
return False
except Exception as e:
colored_print(f"❌ Error checking image dimensions: {e}", Colors.RED)
return False
else:
# Fallback: just check if we got some content
if len(content) > 100: # Assume valid image if we got substantial content
colored_print(
f"✅ Avatar size {size} downloaded (PIL not available for verification)",
Colors.YELLOW,
)
else:
colored_print(
f"❌ Avatar endpoint returned insufficient content for size {size}",
Colors.RED,
)
return False
return True
def test_stats_endpoint(base_url: str) -> bool:
"""Test that the stats endpoint is accessible."""
stats_url = urljoin(base_url, "/stats/")
success, _, _ = make_request(stats_url)
return success
def test_deployment(
base_url: str,
name: str,
max_retries: int = MAX_RETRIES,
retry_delay: int = RETRY_DELAY,
) -> bool:
"""
Test a deployment with retry logic.
Args:
base_url: Base URL of the deployment
name: Human-readable name for the deployment
max_retries: Maximum number of retry attempts
Returns:
True if all tests pass, False otherwise
"""
colored_print(f"Testing {name} deployment at {base_url}", Colors.YELLOW)
for attempt in range(1, max_retries + 1):
colored_print(
f"Attempt {attempt}/{max_retries}: Checking {name} deployment...",
Colors.BLUE,
)
# Check if site is responding
success, version_info = check_version_endpoint(base_url)
if success and version_info:
colored_print(
f"{name} site is responding, checking version...", Colors.GREEN
)
# Display version information
commit_hash = version_info.get("commit_hash", "Unknown")
branch = version_info.get("branch", "Unknown")
version = version_info.get("version", "Unknown")
colored_print(f"Deployed commit: {commit_hash}", Colors.BLUE)
colored_print(f"Deployed branch: {branch}", Colors.BLUE)
colored_print(f"Deployed version: {version}", Colors.BLUE)
# Run functionality tests
colored_print("Running basic functionality tests...", Colors.YELLOW)
# Test avatar redirect
if test_avatar_redirect(base_url):
colored_print("✅ Invalid avatar redirects correctly", Colors.GREEN)
else:
colored_print("❌ Invalid avatar redirect failed", Colors.RED)
return False
# Test avatar sizing
if test_avatar_sizing(base_url):
pass # Success messages are printed within the function
else:
return False
# Test stats endpoint
if test_stats_endpoint(base_url):
colored_print("✅ Stats endpoint working", Colors.GREEN)
else:
colored_print("❌ Stats endpoint failed", Colors.RED)
return False
colored_print(
f"🎉 {name} deployment verification completed successfully!",
Colors.GREEN,
)
return True
else:
colored_print(f"{name} site not responding yet...", Colors.YELLOW)
if attempt < max_retries:
colored_print(
f"Waiting {retry_delay} seconds before next attempt...", Colors.BLUE
)
time.sleep(retry_delay)
colored_print(
f"❌ FAILED: {name} deployment verification timed out after {max_retries} attempts",
Colors.RED,
)
return False
def main():
"""Main function with command-line argument parsing."""
parser = argparse.ArgumentParser(
description="Libravatar Deployment Verification Script",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python3 check_deployment.py --dev # Test dev deployment
python3 check_deployment.py --prod # Test production deployment
python3 check_deployment.py --endpoint <url> # Test custom endpoint
python3 check_deployment.py --dev --prod # Test both deployments
""",
)
parser.add_argument(
"--dev",
action="store_true",
help="Test dev deployment (https://dev.libravatar.org)",
)
parser.add_argument(
"--prod",
action="store_true",
help="Test production deployment (https://libravatar.org)",
)
parser.add_argument("--endpoint", type=str, help="Test custom endpoint URL")
parser.add_argument(
"--max-retries",
type=int,
default=MAX_RETRIES,
help=f"Maximum number of retry attempts (default: {MAX_RETRIES})",
)
parser.add_argument(
"--retry-delay",
type=int,
default=RETRY_DELAY,
help=f"Delay between retry attempts in seconds (default: {RETRY_DELAY})",
)
args = parser.parse_args()
# Validate arguments
if not any([args.dev, args.prod, args.endpoint]):
parser.error("At least one of --dev, --prod, or --endpoint must be specified")
# Update configuration if custom values provided
max_retries = args.max_retries
retry_delay = args.retry_delay
colored_print("Libravatar Deployment Verification Script", Colors.BLUE)
colored_print("=" * 50, Colors.BLUE)
# Check dependencies
if not PIL_AVAILABLE:
colored_print(
"⚠️ Warning: PIL/Pillow not available. Image dimension verification will be limited.",
Colors.YELLOW,
)
colored_print(" Install with: pip install Pillow", Colors.YELLOW)
results = []
# Test dev deployment
if args.dev:
colored_print("", Colors.NC)
dev_result = test_deployment(DEV_URL, "Dev", max_retries, retry_delay)
results.append(("Dev", dev_result))
# Test production deployment
if args.prod:
colored_print("", Colors.NC)
prod_result = test_deployment(PROD_URL, "Production", max_retries, retry_delay)
results.append(("Production", prod_result))
# Test custom endpoint
if args.endpoint:
colored_print("", Colors.NC)
custom_result = test_deployment(
args.endpoint, "Custom", max_retries, retry_delay
)
results.append(("Custom", custom_result))
# Summary
colored_print("", Colors.NC)
colored_print("=" * 50, Colors.BLUE)
colored_print("Deployment Verification Summary:", Colors.BLUE)
colored_print("=" * 50, Colors.BLUE)
all_passed = True
for name, result in results:
if result:
colored_print(f"{name} deployment: PASSED", Colors.GREEN)
else:
colored_print(f"{name} deployment: FAILED", Colors.RED)
all_passed = False
if all_passed:
colored_print("🎉 All deployment verifications passed!", Colors.GREEN)
sys.exit(0)
else:
colored_print("❌ Some deployment verifications failed!", Colors.RED)
sys.exit(1)
if __name__ == "__main__":
main()