mirror of
https://github.com/taylanbakircioglu/haproxy-openmanager.git
synced 2026-09-18 16:45:19 +00:00
281e23ea27
This is a comprehensive update that adds SSL certificate differentiation for frontend (HAProxy bind) and server (backend verification) use cases. FEATURES: - SSL certificates can be marked as 'frontend' or 'server' usage type - Frontend SSL: Private key REQUIRED (for HAProxy bind ssl crt) - Server SSL: Private key OPTIONAL (CA cert only for backend verification) - UI dropdown for usage type selection - Dynamic form validation based on usage type - Filtering: Frontends see only Frontend SSL, Backends see only Server SSL DATABASE: - Added usage_type column to ssl_certificates (default: 'frontend') - Made private_key_content nullable for server SSL support - Migration automatically runs on pod restart BACKEND: - Pydantic v2 compatibility (@field_validator, @model_validator) - SSL router: usage_type filtering support - Agent endpoint: usage_type field included - Improved migration robustness with better error handling - Fixed duplicate ensure_agents_table() function - Fixed JSONB permissions insert with json.dumps() - Fixed ON CONFLICT constraints with explicit checks FRONTEND: - SSL Management: Usage Type dropdown with visual feedback - Frontend Management: Filters only Frontend SSL certificates - Backend Servers: Filters only Server SSL certificates - Dynamic private key validation (required for Frontend, optional for Server) - Improved form UX with color-coded hints AGENT SCRIPTS (Linux & macOS): - Support for Server SSL without private key - Conditional PEM file creation (cert+key vs cert-only) - usage_type awareness in SSL deployment - Backward compatible with existing Frontend SSL certificates DOCKER: - Increased npm timeout for slow networks (300s → 600s) - Increased fetch-retries (5 → 10) - Reduced maxsockets for stability (3 → 1) All changes are backward compatible. Existing SSL certificates default to 'frontend' type and continue working unchanged. Tested with: HAProxy 2.8+, PostgreSQL 15, React 18
124 lines
4.9 KiB
Python
124 lines
4.9 KiB
Python
from pydantic import BaseModel, field_validator, model_validator
|
|
from typing import Optional, List, Dict, Any
|
|
from datetime import datetime
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class SSLCertificateCreate(BaseModel):
|
|
name: str
|
|
certificate_content: str # PEM format certificate
|
|
private_key_content: Optional[str] = None # PEM format private key (optional for server SSL)
|
|
chain_content: Optional[str] = None # PEM format certificate chain (optional)
|
|
cluster_ids: Optional[List[int]] = None # List of cluster IDs for multi-cluster support
|
|
is_global: bool = False # True for global SSL certificates
|
|
usage_type: str = "frontend" # "frontend" or "server" - determines if private key is required
|
|
|
|
@field_validator('usage_type')
|
|
@classmethod
|
|
def validate_usage_type(cls, v):
|
|
if v not in ['frontend', 'server']:
|
|
raise ValueError('usage_type must be either "frontend" or "server"')
|
|
return v
|
|
|
|
@field_validator('certificate_content')
|
|
@classmethod
|
|
def validate_certificate(cls, v):
|
|
if not v or not v.strip():
|
|
raise ValueError('Certificate content is required')
|
|
|
|
# Basic PEM format check
|
|
v = v.strip()
|
|
if '-----BEGIN CERTIFICATE-----' not in v or '-----END CERTIFICATE-----' not in v:
|
|
raise ValueError('Certificate must be in PEM format')
|
|
|
|
return v
|
|
|
|
@model_validator(mode='after')
|
|
def validate_private_key_based_on_usage(self):
|
|
"""Private key is required for frontend SSL, optional for server SSL"""
|
|
usage_type = self.usage_type
|
|
private_key = self.private_key_content
|
|
|
|
if usage_type == 'frontend':
|
|
# Frontend SSL requires private key
|
|
if not private_key or not private_key.strip():
|
|
raise ValueError('Private key is required for frontend SSL certificates')
|
|
|
|
# Basic PEM format check
|
|
private_key = private_key.strip()
|
|
if '-----BEGIN' not in private_key or '-----END' not in private_key:
|
|
raise ValueError('Private key must be in PEM format')
|
|
elif usage_type == 'server':
|
|
# Server SSL - private key is optional
|
|
if private_key and private_key.strip():
|
|
# If provided, validate format
|
|
private_key = private_key.strip()
|
|
if '-----BEGIN' not in private_key or '-----END' not in private_key:
|
|
raise ValueError('Private key must be in PEM format')
|
|
|
|
return self
|
|
|
|
@field_validator('chain_content')
|
|
@classmethod
|
|
def validate_chain(cls, v):
|
|
if v and v.strip():
|
|
# Basic PEM format check for chain
|
|
if '-----BEGIN CERTIFICATE-----' not in v or '-----END CERTIFICATE-----' not in v:
|
|
raise ValueError('Certificate chain must be in PEM format')
|
|
|
|
return v
|
|
|
|
class SSLCertificateUpdate(BaseModel):
|
|
name: Optional[str] = None
|
|
certificate_content: Optional[str] = None
|
|
private_key_content: Optional[str] = None
|
|
chain_content: Optional[str] = None
|
|
cluster_id: Optional[int] = None
|
|
usage_type: Optional[str] = None # "frontend" or "server"
|
|
|
|
@field_validator('usage_type')
|
|
@classmethod
|
|
def validate_usage_type(cls, v):
|
|
if v is not None and v not in ['frontend', 'server']:
|
|
raise ValueError('usage_type must be either "frontend" or "server"')
|
|
return v
|
|
|
|
class SSLCertificate(BaseModel):
|
|
id: int
|
|
name: str
|
|
domain: str # Auto-parsed from certificate
|
|
all_domains: List[str] = [] # All domains from SAN + CN
|
|
certificate_content: str
|
|
private_key_content: Optional[str] = None # Optional for server SSL
|
|
chain_content: Optional[str] = None
|
|
expiry_date: Optional[datetime] = None # Auto-parsed from certificate
|
|
issuer: Optional[str] = None # Auto-parsed from certificate
|
|
status: str # 'valid', 'expiring_soon', 'expired', 'invalid'
|
|
days_until_expiry: int = 0
|
|
fingerprint: Optional[str] = None
|
|
cluster_id: Optional[int] = None
|
|
usage_type: str = "frontend" # "frontend" or "server"
|
|
created_at: Optional[datetime] = None
|
|
updated_at: Optional[datetime] = None
|
|
has_pending_config: bool = False
|
|
|
|
# Certificate details for UI display
|
|
certificate_info: Optional[Dict[str, Any]] = None
|
|
|
|
class SSLCertificateResponse(BaseModel):
|
|
"""Response model for SSL certificate API endpoints"""
|
|
id: int
|
|
name: str
|
|
domain: str
|
|
all_domains: List[str] = []
|
|
status: str
|
|
days_until_expiry: int = 0
|
|
expiry_date: Optional[datetime] = None
|
|
issuer: Optional[str] = None
|
|
fingerprint: Optional[str] = None
|
|
cluster_id: Optional[int] = None
|
|
usage_type: str = "frontend" # "frontend" or "server"
|
|
created_at: Optional[datetime] = None
|
|
updated_at: Optional[datetime] = None
|
|
has_pending_config: bool = False |