Add upstream caching infrastructure and refactor CI pipeline
Upstream Caching (Epic #68-#75, #105): - Add upstream_sources and cache_settings tables with migrations - Add cache management API endpoints (CRUD for sources, settings) - Add environment variable overrides for upstream sources and cache settings - Add encryption module for storing credentials securely - Add frontend Admin Cache Management page - Add is_system field to projects for system cache distinction - Add purge_seed_data for transitioning to production-like environments CI Pipeline Refactoring: - Remove reset jobs (reset_stage_pre, reset_stage) - Add ephemeral orchard-test deployment for main branch testing - Run integration tests on ephemeral deployment before promoting to stage - Stage is now long-running pre-prod (smoke tests only) - Disable prosper_setup for tag pipelines
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
from functools import lru_cache
|
||||
from typing import Optional
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
@@ -56,6 +59,12 @@ class Settings(BaseSettings):
|
||||
# Initial admin user settings
|
||||
admin_password: str = "" # Initial admin password (if empty, uses 'changeme123')
|
||||
|
||||
# Cache settings
|
||||
cache_encryption_key: str = "" # Fernet key for encrypting upstream credentials (auto-generated if empty)
|
||||
# Global cache settings overrides (None = use DB value, True/False = override DB)
|
||||
cache_allow_public_internet: Optional[bool] = None # Override allow_public_internet (air-gap mode)
|
||||
cache_auto_create_system_projects: Optional[bool] = None # Override auto_create_system_projects
|
||||
|
||||
# JWT Authentication settings (optional, for external identity providers)
|
||||
jwt_enabled: bool = False # Enable JWT token validation
|
||||
jwt_secret: str = "" # Secret key for HS256, or leave empty for RS256 with JWKS
|
||||
@@ -88,3 +97,113 @@ class Settings(BaseSettings):
|
||||
@lru_cache()
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
|
||||
|
||||
class EnvUpstreamSource:
|
||||
"""Represents an upstream source defined via environment variables."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
url: str,
|
||||
source_type: str = "generic",
|
||||
enabled: bool = True,
|
||||
is_public: bool = True,
|
||||
auth_type: str = "none",
|
||||
username: Optional[str] = None,
|
||||
password: Optional[str] = None,
|
||||
priority: int = 100,
|
||||
):
|
||||
self.name = name
|
||||
self.url = url
|
||||
self.source_type = source_type
|
||||
self.enabled = enabled
|
||||
self.is_public = is_public
|
||||
self.auth_type = auth_type
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.priority = priority
|
||||
self.source = "env" # Mark as env-defined
|
||||
|
||||
|
||||
def parse_upstream_sources_from_env() -> list[EnvUpstreamSource]:
|
||||
"""
|
||||
Parse upstream sources from environment variables.
|
||||
|
||||
Uses double underscore (__) as separator to allow source names with single underscores.
|
||||
Pattern: ORCHARD_UPSTREAM__{NAME}__FIELD
|
||||
|
||||
Example:
|
||||
ORCHARD_UPSTREAM__NPM_PRIVATE__URL=https://npm.corp.com
|
||||
ORCHARD_UPSTREAM__NPM_PRIVATE__TYPE=npm
|
||||
ORCHARD_UPSTREAM__NPM_PRIVATE__ENABLED=true
|
||||
ORCHARD_UPSTREAM__NPM_PRIVATE__AUTH_TYPE=basic
|
||||
ORCHARD_UPSTREAM__NPM_PRIVATE__USERNAME=reader
|
||||
ORCHARD_UPSTREAM__NPM_PRIVATE__PASSWORD=secret
|
||||
|
||||
Returns:
|
||||
List of EnvUpstreamSource objects parsed from environment variables.
|
||||
"""
|
||||
# Pattern: ORCHARD_UPSTREAM__{NAME}__{FIELD}
|
||||
pattern = re.compile(r"^ORCHARD_UPSTREAM__([A-Z0-9_]+)__([A-Z_]+)$", re.IGNORECASE)
|
||||
|
||||
# Collect all env vars matching the pattern, grouped by source name
|
||||
sources_data: dict[str, dict[str, str]] = {}
|
||||
|
||||
for key, value in os.environ.items():
|
||||
match = pattern.match(key)
|
||||
if match:
|
||||
source_name = match.group(1).lower() # Normalize to lowercase
|
||||
field = match.group(2).upper()
|
||||
if source_name not in sources_data:
|
||||
sources_data[source_name] = {}
|
||||
sources_data[source_name][field] = value
|
||||
|
||||
# Build source objects from collected data
|
||||
sources: list[EnvUpstreamSource] = []
|
||||
|
||||
for name, data in sources_data.items():
|
||||
# URL is required
|
||||
url = data.get("URL")
|
||||
if not url:
|
||||
continue # Skip sources without URL
|
||||
|
||||
# Parse boolean fields
|
||||
def parse_bool(val: Optional[str], default: bool) -> bool:
|
||||
if val is None:
|
||||
return default
|
||||
return val.lower() in ("true", "1", "yes", "on")
|
||||
|
||||
# Parse integer fields
|
||||
def parse_int(val: Optional[str], default: int) -> int:
|
||||
if val is None:
|
||||
return default
|
||||
try:
|
||||
return int(val)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
source = EnvUpstreamSource(
|
||||
name=name.replace("_", "-"), # Convert underscores to hyphens for readability
|
||||
url=url,
|
||||
source_type=data.get("TYPE", "generic").lower(),
|
||||
enabled=parse_bool(data.get("ENABLED"), True),
|
||||
is_public=parse_bool(data.get("IS_PUBLIC"), True),
|
||||
auth_type=data.get("AUTH_TYPE", "none").lower(),
|
||||
username=data.get("USERNAME"),
|
||||
password=data.get("PASSWORD"),
|
||||
priority=parse_int(data.get("PRIORITY"), 100),
|
||||
)
|
||||
sources.append(source)
|
||||
|
||||
return sources
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def get_env_upstream_sources() -> tuple[EnvUpstreamSource, ...]:
|
||||
"""
|
||||
Get cached list of upstream sources from environment variables.
|
||||
|
||||
Returns a tuple for hashability (required by lru_cache).
|
||||
"""
|
||||
return tuple(parse_upstream_sources_from_env())
|
||||
|
||||
Reference in New Issue
Block a user