Structum Bootstrap (structum-bootstrap)¶
Structum Bootstrap fornisce meccanismi di “Fail Fast” per validare l’ambiente di esecuzione all’avvio dell’applicazione.
Feature |
Stato |
Versione |
|---|---|---|
Stato |
Alpha |
0.1.0 |
Namespace |
|
|
Dipendenze |
|
Indice¶
1. Cos’è Bootstrap Plugin¶
structum-bootstrap implementa la filosofia “Professional Paranoia”: verifica esplicita dello stato runtime prima di avviare l’applicazione.
1.1 Problema Risolto¶
Prima (senza plugin):
# ❌ Assunzioni implicite
app.run()
# Runtime errors dopo deploy:
# - "DATABASE_URL not set"
# - "/var/log/app not writable"
# - "Python 3.8 required, found 3.7"
Dopo (con plugin):
# ✅ Validazione esplicita
boot = SystemBootstrapper()
boot.add_validator(EnvValidator(required=["DATABASE_URL"]))
boot.add_validator(DirectoryValidator("/var/log/app", writable=True))
boot.add_validator(PythonVersionValidator(min_version="3.9"))
boot.run_or_exit() # Fails FAST con report dettagliato
# Se arriviamo qui, l'ambiente è GARANTITO valido
app.run()
1.2 Caratteristiche Principali¶
Feature |
Descrizione |
|---|---|
Fail Fast |
Errori a startup, non durante runtime |
Detailed Reports |
Report testuale con tutti i check |
Type Safe |
Validators basati su Protocol |
Extensible |
Crea custom validators facilmente |
Zero Secrets Leak |
Valida esistenza ENV vars senza loggare valori |
Production Ready |
Validazione filesystem, network, database connectivity |
1.3 Philosophy: Explicit > Implicit¶
┌─────────────────────────────────────────┐
│ Traditional Approach (Implicit) │
├─────────────────────────────────────────┤
│ app.run() │
│ ├─ Assume DB_URL exists │
│ ├─ Assume /logs writable │
│ └─ Hope for the best │
│ │
│ Result: Runtime errors in production! │
└─────────────────────────────────────────┘
┌─────────────────────────────────────────┐
│ Structum Approach (Explicit) │
├─────────────────────────────────────────┤
│ boot.validate() │
│ ├─ CHECK DB_URL exists │
│ ├─ CHECK /logs writable │
│ └─ CHECK Python version │
│ │
│ ONLY THEN: app.run() │
│ │
│ Result: Issues found BEFORE deploy! │
└─────────────────────────────────────────┘
2. Concetti Fondamentali¶
2.1 SystemBootstrapper¶
L’orchestratore centrale che esegue validators in sequenza:
bootstrapper = SystemBootstrapper()
bootstrapper.add_validator(validator1)
bootstrapper.add_validator(validator2)
# Run validation
result = bootstrapper.run()
if not result.success:
print(result.report)
sys.exit(1)
2.2 Validator Protocol¶
Ogni validator implementa questo contract:
from typing import Protocol
class Validator(Protocol):
"""Protocol per validatori custom."""
def validate(self, context: ValidationContext) -> None:
"""
Esegue validazione.
Registra risultati in context.
"""
...
2.3 ValidationContext¶
Container per risultati di validazione:
context = ValidationContext()
# Registra check success
context.add_check("Database", success=True, message="Connected OK")
# Registra check failure
context.add_check("Redis", success=False, message="Connection timeout")
# Verifica stato
if context.has_failures():
print(context.get_report())
3. Quick Start (5 Minuti)¶
Step 1: Installazione¶
pip install -e packages/bootstrap
Step 2: Basic Validation¶
from structum_lab.plugins.bootstrap import (
SystemBootstrapper,
EnvValidator,
PythonVersionValidator
)
def main():
# 1. Create bootstrapper
boot = SystemBootstrapper()
# 2. Add validators
boot.add_validator(
EnvValidator(required=["DATABASE_URL", "API_KEY"])
)
boot.add_validator(
PythonVersionValidator(min_version="3.9")
)
# 3. Run validation
boot.run_or_exit() # Exits with code 1 if validation fails
# 4. Start application (guaranteed valid environment)
print("✅ Environment validated - starting application")
start_application()
if __name__ == "__main__":
main()
Step 3: Test Validation¶
# Missing env var - should fail
python main.py
# Output:
# ❌ Bootstrap Validation Failed
#
# Environment Variables:
# ✗ DATABASE_URL: Missing
# ✗ API_KEY: Missing
#
# Python Version:
# ✓ Version 3.11.0 >= 3.9 (required)
# Set env vars
export DATABASE_URL=postgresql://localhost/mydb
export API_KEY=secret-key-123
python main.py
# Output:
# ✅ Environment validated - starting application
4. Validators Built-in¶
4.1 EnvValidator¶
Valida esistenza di environment variables.
from structum_lab.plugins.bootstrap import EnvValidator
# Required vars
validator = EnvValidator(required=["DB_URL", "API_KEY"])
# Optional vars (warns if missing)
validator = EnvValidator(
required=["DB_URL"],
optional=["REDIS_URL", "CACHE_TTL"]
)
# Custom names for reporting
validator = EnvValidator(
required={
"DATABASE_URL": "Primary Database Connection",
"REDIS_URL": "Cache Server Connection"
}
)
Security Note: validator logs only presence, never values.
✓ DATABASE_URL: Present
✗ API_KEY: Missing
4.2 PythonVersionValidator¶
Valida versione Python runtime.
from structum_lab.plugins.bootstrap import PythonVersionValidator
# Minimum version
validator = PythonVersionValidator(min_version="3.9")
# Exact version range
validator = PythonVersionValidator(
min_version="3.9",
max_version="3.11"
)
# Specific versions
validator = PythonVersionValidator(
allowed_versions=["3.10", "3.11"]
)
4.3 DirectoryValidator¶
Valida esistenza e permessi directory.
from structum_lab.plugins.bootstrap import DirectoryValidator
# Check existence
validator = DirectoryValidator("/var/log/myapp")
# Check writable
validator = DirectoryValidator(
"/var/log/myapp",
writable=True
)
# Check readable
validator = DirectoryValidator(
"/etc/myapp",
readable=True
)
# Auto-create if missing
validator = DirectoryValidator(
"/var/log/myapp",
writable=True,
create_if_missing=True
)
4.4 ConfigValidator¶
Valida che Structum config si carichi correttamente.
from structum_lab.plugins.bootstrap import ConfigValidator
# Basic validation
validator = ConfigValidator()
# Validate specific keys exist
validator = ConfigValidator(
required_keys=["database.url", "app.name"]
)
# Validate config provider
validator = ConfigValidator(
provider_class=DynaconfConfigProvider
)
4.5 DatabaseValidator¶
Valida connessione database.
from structum_lab.plugins.bootstrap import DatabaseValidator
# Basic connectivity check
validator = DatabaseValidator(
url="postgresql://localhost/mydb"
)
# With timeout
validator = DatabaseValidator(
url="postgresql://localhost/mydb",
timeout=5.0 # seconds
)
# From config
validator = DatabaseValidator.from_config()
4.6 FileValidator¶
Valida esistenza e permessi file.
from structum_lab.plugins.bootstrap import FileValidator
# Check file exists
validator = FileValidator("/etc/myapp/config.toml")
# Check readable
validator = FileValidator(
"/etc/myapp/config.toml",
readable=True
)
# Check specific size constraints
validator = FileValidator(
"/var/lib/myapp/data.db",
max_size_mb=1000
)
5. Custom Validators¶
5.1 Simple Custom Validator¶
from structum_lab.plugins.bootstrap import Validator, ValidationContext
class PortAvailableValidator(Validator):
"""Check if network port is available."""
def __init__(self, port: int):
self.port = port
def validate(self, context: ValidationContext) -> None:
import socket
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('localhost', self.port))
sock.close()
context.add_check(
f"Port {self.port}",
success=True,
message=f"Available"
)
except OSError:
context.add_check(
f"Port {self.port}",
success=False,
message=f"Already in use"
)
# Usage
boot.add_validator(PortAvailableValidator(8000))
5.2 Advanced Custom Validator¶
from datetime import datetime, timedelta
class SSLCertificateValidator(Validator):
"""Validate SSL certificate is not expired."""
def __init__(self, cert_path: str, warn_days: int = 30):
self.cert_path = cert_path
self.warn_days = warn_days
def validate(self, context: ValidationContext) -> None:
from pathlib import Path
import ssl
import OpenSSL
cert_file = Path(self.cert_path)
if not cert_file.exists():
context.add_check(
"SSL Certificate",
success=False,
message=f"File not found: {self.cert_path}"
)
return
# Load certificate
with open(cert_file, 'rb') as f:
cert_data = f.read()
cert = OpenSSL.crypto.load_certificate(
OpenSSL.crypto.FILETYPE_PEM,
cert_data
)
# Check expiration
not_after = datetime.strptime(
cert.get_notAfter().decode('ascii'),
'%Y%m%d%H%M%SZ'
)
days_until_expiry = (not_after - datetime.utcnow()).days
if days_until_expiry < 0:
context.add_check(
"SSL Certificate",
success=False,
message=f"Expired {abs(days_until_expiry)} days ago"
)
elif days_until_expiry < self.warn_days:
context.add_check(
"SSL Certificate",
success=True,
message=f"⚠️ Expires in {days_until_expiry} days (warning threshold: {self.warn_days})"
)
else:
context.add_check(
"SSL Certificate",
success=True,
message=f"Valid for {days_until_expiry} days"
)
5.3 Async Validator¶
import asyncio
class AsyncDatabaseValidator(Validator):
"""Async database connectivity check."""
def __init__(self, url: str):
self.url = url
def validate(self, context: ValidationContext) -> None:
# Run async code in sync context
result = asyncio.run(self._async_check())
context.add_check(
"Database (Async)",
success=result["success"],
message=result["message"]
)
async def _async_check(self) -> dict:
try:
import asyncpg
conn = await asyncpg.connect(self.url, timeout=5.0)
await conn.close()
return {
"success": True,
"message": "Connected successfully"
}
except Exception as e:
return {
"success": False,
"message": f"Connection failed: {e}"
}
6. Bootstrap Context¶
6.1 Accessing Context¶
from structum_lab.plugins.bootstrap import ValidationContext
context = ValidationContext()
# Add checks
context.add_check("Service A", True, "OK")
context.add_check("Service B", False, "Failed to connect")
# Query status
print(f"Total checks: {context.total_checks}")
print(f"Successful: {context.successful_checks}")
print(f"Failed: {context.failed_checks}")
# Get detailed report
print(context.get_report())
6.2 Report Format¶
┌─────────────────────────────────────┐
│ Bootstrap Validation Report │
├─────────────────────────────────────┤
│ Status: ❌ FAILED (5/7 checks passed)│
└─────────────────────────────────────┘
Environment Variables:
✓ DATABASE_URL: Present
✓ API_KEY: Present
✗ REDIS_URL: Missing
Python Version:
✓ Version 3.11.0 >= 3.9 (required)
Directories:
✓ /var/log/myapp: Exists and writable
✗ /etc/ssl/certs/app.pem: Not readable
6.3 Programmatic Access¶
# Check specific results
for check in context.checks:
print(f"{check.name}: {check.success}")
if not check.success:
print(f" Error: {check.message}")
# Export to JSON
import json
report_json = {
"total": context.total_checks,
"passed": context.successful_checks,
"failed": context.failed_checks,
"checks": [
{
"name": check.name,
"success": check.success,
"message": check.message
}
for check in context.checks
]
}
print(json.dumps(report_json, indent=2))
7. Error Handling¶
7.1 Exit on Failure (Production)¶
# Production mode: exit immediately on failure
boot = SystemBootstrapper()
boot.add_validator(EnvValidator(required=["DB_URL"]))
boot.run_or_exit() # sys.exit(1) if validation fails
# This line only executes if validation passed
start_application()
7.2 Continue on Failure (Development)¶
# Development mode: collect all errors
boot = SystemBootstrapper()
boot.add_validator(EnvValidator(required=["DB_URL"]))
boot.add_validator(DirectoryValidator("/var/log"))
result = boot.run()
if not result.success:
print("⚠️ Found validation issues:")
print(result.report)
# Developer can decide whether to continue
if input("Continue anyway? (y/n): ").lower() != 'y':
sys.exit(1)
start_application()
7.3 Custom Error Handling¶
class CustomBootstrapper(SystemBootstrapper):
"""Custom bootstrapper con logging."""
def on_validation_start(self):
"""Called before validation starts."""
logger.info("Starting bootstrap validation")
def on_validation_complete(self, context: ValidationContext):
"""Called after all validators run."""
logger.info(
f"Validation complete: {context.successful_checks}/{context.total_checks} passed"
)
if context.has_failures():
logger.error(f"Validation failed:\n{context.get_report()}")
# Send to monitoring
metrics.increment("bootstrap.failures")
def on_validator_error(self, validator: Validator, error: Exception):
"""Called if validator raises exception."""
logger.exception(f"Validator {validator.__class__.__name__} crashed: {error}")
8. Advanced Patterns¶
8.1 Conditional Validation¶
import os
boot = SystemBootstrapper()
# Always validate
boot.add_validator(PythonVersionValidator(min_version="3.9"))
# Production-only validators
if os.getenv("ENV") == "production":
boot.add_validator(SSLCertificateValidator("/etc/ssl/app.pem"))
boot.add_validator(DirectoryValidator("/var/log", writable=True))
# Development-only validators
if os.getenv("ENV") == "development":
boot.add_validator(PortAvailableValidator(8000))
boot.run_or_exit()
8.2 Validator Groups¶
class InfrastructureValidators:
"""Group of infrastructure validators."""
@staticmethod
def get_all() -> list[Validator]:
return [
EnvValidator(required=["DB_URL", "REDIS_URL"]),
DatabaseValidator.from_config(),
PortAvailableValidator(5432), # PostgreSQL
PortAvailableValidator(6379), # Redis
]
class SecurityValidators:
"""Group of security validators."""
@staticmethod
def get_all() -> list[Validator]:
return [
SSLCertificateValidator("/etc/ssl/app.pem"),
FileValidator("/etc/ssl/private/app.key", readable=True),
EnvValidator(required=["JWT_SECRET", "ENCRYPTION_KEY"]),
]
# Usage
boot = SystemBootstrapper()
for validator in InfrastructureValidators.get_all():
boot.add_validator(validator)
for validator in SecurityValidators.get_all():
boot.add_validator(validator)
boot.run_or_exit()
8.3 Progressive Validation¶
def bootstrap_application():
"""Multi-stage bootstrap con fail-fast."""
# Stage 1: Critical checks (fail immediately)
stage1 = SystemBootstrapper()
stage1.add_validator(PythonVersionValidator(min_version="3.9"))
stage1.add_validator(EnvValidator(required=["DATABASE_URL"]))
stage1.run_or_exit()
print("✓ Stage 1: Critical checks passed")
# Stage 2: Infrastructure (can warn)
stage2 = SystemBootstrapper()
stage2.add_validator(DatabaseValidator.from_config())
stage2.add_validator(DirectoryValidator("/var/log", create_if_missing=True))
result = stage2.run()
if not result.success:
print(f"⚠️ Stage 2: Infrastructure warnings:\n{result.report}")
print("✓ Stage 2: Infrastructure checks completed")
# Stage 3: Optional features
stage3 = SystemBootstrapper()
stage3.add_validator(EnvValidator(optional=["REDIS_URL", "CACHE_TTL"]))
stage3.run() # Never fails
print("✓ Stage 3: Optional features validated")
print("✅ Bootstrap complete - starting application")
9. Production Deployment¶
9.1 Container Health Checks¶
# health_check.py
from structum_lab.plugins.bootstrap import SystemBootstrapper, DatabaseValidator
def health_check() -> bool:
"""Health check per Kubernetes/Docker."""
boot = SystemBootstrapper()
boot.add_validator(DatabaseValidator.from_config())
result = boot.run()
return result.success
if __name__ == "__main__":
import sys
sys.exit(0 if health_check() else 1)
Dockerfile:
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD python health_check.py || exit 1
9.2 Systemd Integration¶
# /etc/systemd/system/myapp.service
[Unit]
Description=My Application
After=network.target postgresql.service
[Service]
Type=simple
ExecStartPre=/usr/bin/python3 /app/bootstrap.py
ExecStart=/usr/bin/python3 /app/main.py
Restart=on-failure
[Install]
WantedBy=multi-user.target
9.3 CI/CD Integration¶
# .github/workflows/deploy.yml
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Pre-deployment Validation
run: |
python -m structum_lab.plugins.bootstrap \
--validator env:DATABASE_URL,API_KEY \
--validator python:3.9+ \
--validator dir:/var/log/app:writable
- name: Deploy Application
if: success()
run: ./deploy.sh
10. API Reference¶
10.1 SystemBootstrapper¶
class SystemBootstrapper:
"""Main bootstrapper orchestrator."""
def add_validator(self, validator: Validator) -> None:
"""Add a validator to the bootstrap sequence."""
...
def run(self) -> ValidationResult:
"""
Run all validators.
Returns ValidationResult with success status.
"""
...
def run_or_exit(self, exit_code: int = 1) -> None:
"""
Run validators and exit if validation fails.
Use in production for fail-fast behavior.
"""
...
10.2 Built-in Validators¶
class EnvValidator(Validator):
def __init__(
self,
required: list[str] | dict[str, str] = None,
optional: list[str] | dict[str, str] = None
):
...
class PythonVersionValidator(Validator):
def __init__(
self,
min_version: str = None,
max_version: str = None,
allowed_versions: list[str] = None
):
...
class DirectoryValidator(Validator):
def __init__(
self,
path: str,
readable: bool = True,
writable: bool = False,
create_if_missing: bool = False
):
...
class DatabaseValidator(Validator):
def __init__(self, url: str, timeout: float = 5.0):
...
@classmethod
def from_config(cls, key: str = "database.url") -> "DatabaseValidator":
...
10.3 ValidationContext¶
class ValidationContext:
"""Container for validation results."""
def add_check(self, name: str, success: bool, message: str) -> None:
"""Register a validation check result."""
...
def has_failures(self) -> bool:
"""Check if any validation failed."""
...
def get_report(self) -> str:
"""Get formatted text report."""
...
@property
def total_checks(self) -> int:
...
@property
def successful_checks(self) -> int:
...
@property
def failed_checks(self) -> int:
...
See Also¶
Configuration Module - Config system integration
Dynaconf Plugin - Configuration provider
Observability Plugin - Logging and metrics