145 lines
No EOL
5 KiB
Python
145 lines
No EOL
5 KiB
Python
"""Enhanced configuration system with Secret Manager integration."""
|
|
|
|
import os
|
|
import asyncio
|
|
from typing import Dict, Optional, Any
|
|
from functools import lru_cache
|
|
from pydantic_settings import BaseSettings
|
|
|
|
from .config import Settings as BaseConfig
|
|
from .logging import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
class SecretsConfig(BaseConfig):
|
|
"""Enhanced configuration that loads secrets from GCP Secret Manager."""
|
|
|
|
def __init__(self, **kwargs):
|
|
# Initialize with base configuration first
|
|
super().__init__(**kwargs)
|
|
|
|
# Flag to track if secrets have been loaded
|
|
self._secrets_loaded = False
|
|
self._secret_values: Dict[str, str] = {}
|
|
|
|
async def load_secrets(self) -> None:
|
|
"""Load secrets from Secret Manager asynchronously."""
|
|
if self._secrets_loaded:
|
|
return
|
|
|
|
try:
|
|
# Only import here to avoid circular imports
|
|
from app.services.secrets_manager import secrets_manager
|
|
|
|
# Define which config fields should be loaded from secrets
|
|
secret_mappings = {
|
|
# Config field -> Secret Manager name
|
|
"jwt_secret": "jwt-secret",
|
|
"jwt_refresh_secret": "jwt-refresh-secret",
|
|
"mongodb_uri": "mongodb-url",
|
|
"redis_url": "redis-url",
|
|
"gemini_api_key": "gemini-api-key",
|
|
"sendgrid_api_key": "sendgrid-api-key",
|
|
"elevenlabs_api_key": "elevenlabs-api-key",
|
|
"sentry_dsn": "sentry-dsn"
|
|
}
|
|
|
|
# Get all secrets in batch
|
|
secret_names = list(secret_mappings.values())
|
|
retrieved_secrets = await secrets_manager.get_secrets_batch(secret_names)
|
|
|
|
# Map secrets back to config fields
|
|
for config_field, secret_name in secret_mappings.items():
|
|
if secret_name in retrieved_secrets:
|
|
self._secret_values[config_field] = retrieved_secrets[secret_name]
|
|
# Override the config value
|
|
setattr(self, config_field, retrieved_secrets[secret_name])
|
|
logger.debug(f"Loaded secret for {config_field}")
|
|
else:
|
|
logger.warning(f"Secret {secret_name} not available, using environment/default")
|
|
|
|
self._secrets_loaded = True
|
|
logger.info(f"Successfully loaded {len(retrieved_secrets)} secrets from Secret Manager")
|
|
|
|
except Exception as e:
|
|
logger.warning(f"Failed to load secrets from Secret Manager: {e}")
|
|
logger.warning("Falling back to environment variables")
|
|
self._secrets_loaded = True # Mark as loaded to prevent retries
|
|
|
|
def get_secret_value(self, field_name: str) -> Optional[str]:
|
|
"""Get a secret value if it was loaded from Secret Manager."""
|
|
return self._secret_values.get(field_name)
|
|
|
|
async def refresh_secrets(self) -> None:
|
|
"""Force refresh secrets from Secret Manager."""
|
|
self._secrets_loaded = False
|
|
self._secret_values.clear()
|
|
|
|
# Clear the secrets manager cache
|
|
from app.services.secrets_manager import secrets_manager
|
|
secrets_manager.clear_cache()
|
|
|
|
await self.load_secrets()
|
|
|
|
@property
|
|
def is_production(self) -> bool:
|
|
"""Check if running in production environment."""
|
|
return self.app_env == "prod"
|
|
|
|
@property
|
|
def is_development(self) -> bool:
|
|
"""Check if running in development environment."""
|
|
return self.app_env == "dev"
|
|
|
|
@property
|
|
def google_cloud_project(self) -> str:
|
|
"""Get Google Cloud Project ID."""
|
|
return self.gcp_project_id
|
|
|
|
@property
|
|
def jwt_refresh_secret(self) -> str:
|
|
"""Get JWT refresh secret (fallback to main secret if not set)."""
|
|
return getattr(self, '_jwt_refresh_secret', self.jwt_secret)
|
|
|
|
@jwt_refresh_secret.setter
|
|
def jwt_refresh_secret(self, value: str) -> None:
|
|
"""Set JWT refresh secret."""
|
|
self._jwt_refresh_secret = value
|
|
|
|
|
|
# Global configuration instance
|
|
_config_instance: Optional[SecretsConfig] = None
|
|
|
|
|
|
async def initialize_config() -> SecretsConfig:
|
|
"""Initialize configuration with secrets loading."""
|
|
global _config_instance
|
|
|
|
if _config_instance is None:
|
|
_config_instance = SecretsConfig()
|
|
await _config_instance.load_secrets()
|
|
|
|
return _config_instance
|
|
|
|
|
|
def get_settings() -> SecretsConfig:
|
|
"""Get settings instance (synchronous)."""
|
|
global _config_instance
|
|
|
|
if _config_instance is None:
|
|
# Initialize without secrets for backwards compatibility
|
|
_config_instance = SecretsConfig()
|
|
logger.warning("Settings accessed before async initialization - secrets not loaded")
|
|
|
|
return _config_instance
|
|
|
|
|
|
@lru_cache()
|
|
def get_settings_cached() -> SecretsConfig:
|
|
"""Get cached settings instance."""
|
|
return get_settings()
|
|
|
|
|
|
# Backwards compatibility
|
|
settings = get_settings() |