Fix build and code structure improvements. New but essential UI functionality. CI improvements. Documentation improvements. AI module improvements.
This commit is contained in:
373
.gitea/scripts/release/generate_compose.py
Normal file
373
.gitea/scripts/release/generate_compose.py
Normal file
@@ -0,0 +1,373 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
generate_compose.py - Generate pinned Docker Compose files for suite releases
|
||||
|
||||
Sprint: CI/CD Enhancement - Suite Release Pipeline
|
||||
Creates docker-compose.yml files with pinned image versions for releases.
|
||||
|
||||
Usage:
|
||||
python generate_compose.py <version> <codename> [options]
|
||||
python generate_compose.py 2026.04 Nova --output docker-compose.yml
|
||||
python generate_compose.py 2026.04 Nova --airgap --output docker-compose.airgap.yml
|
||||
|
||||
Arguments:
|
||||
version Suite version (YYYY.MM format)
|
||||
codename Release codename
|
||||
|
||||
Options:
|
||||
--output FILE Output file (default: stdout)
|
||||
--airgap Generate air-gap variant
|
||||
--registry URL Container registry URL
|
||||
--include-deps Include infrastructure dependencies (postgres, valkey)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
# Repository paths
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
REPO_ROOT = SCRIPT_DIR.parent.parent.parent
|
||||
MANIFEST_FILE = REPO_ROOT / "devops" / "releases" / "service-versions.json"
|
||||
|
||||
# Default registry
|
||||
DEFAULT_REGISTRY = "git.stella-ops.org/stella-ops.org"
|
||||
|
||||
# Service definitions with port mappings and dependencies
|
||||
SERVICE_DEFINITIONS = {
|
||||
"authority": {
|
||||
"ports": ["8080:8080"],
|
||||
"depends_on": ["postgres"],
|
||||
"environment": {
|
||||
"AUTHORITY_DB_CONNECTION": "Host=postgres;Database=authority;Username=stellaops;Password=${POSTGRES_PASSWORD}",
|
||||
},
|
||||
"healthcheck": {
|
||||
"test": ["CMD", "curl", "-f", "http://localhost:8080/health"],
|
||||
"interval": "30s",
|
||||
"timeout": "10s",
|
||||
"retries": 3,
|
||||
},
|
||||
},
|
||||
"attestor": {
|
||||
"ports": ["8081:8080"],
|
||||
"depends_on": ["postgres", "authority"],
|
||||
"environment": {
|
||||
"ATTESTOR_DB_CONNECTION": "Host=postgres;Database=attestor;Username=stellaops;Password=${POSTGRES_PASSWORD}",
|
||||
"ATTESTOR_AUTHORITY_URL": "http://authority:8080",
|
||||
},
|
||||
},
|
||||
"concelier": {
|
||||
"ports": ["8082:8080"],
|
||||
"depends_on": ["postgres", "valkey"],
|
||||
"environment": {
|
||||
"CONCELIER_DB_CONNECTION": "Host=postgres;Database=concelier;Username=stellaops;Password=${POSTGRES_PASSWORD}",
|
||||
"CONCELIER_CACHE_URL": "valkey:6379",
|
||||
},
|
||||
},
|
||||
"scanner": {
|
||||
"ports": ["8083:8080"],
|
||||
"depends_on": ["postgres", "concelier"],
|
||||
"environment": {
|
||||
"SCANNER_DB_CONNECTION": "Host=postgres;Database=scanner;Username=stellaops;Password=${POSTGRES_PASSWORD}",
|
||||
"SCANNER_CONCELIER_URL": "http://concelier:8080",
|
||||
},
|
||||
"volumes": ["/var/run/docker.sock:/var/run/docker.sock:ro"],
|
||||
},
|
||||
"policy": {
|
||||
"ports": ["8084:8080"],
|
||||
"depends_on": ["postgres"],
|
||||
"environment": {
|
||||
"POLICY_DB_CONNECTION": "Host=postgres;Database=policy;Username=stellaops;Password=${POSTGRES_PASSWORD}",
|
||||
},
|
||||
},
|
||||
"signer": {
|
||||
"ports": ["8085:8080"],
|
||||
"depends_on": ["authority"],
|
||||
"environment": {
|
||||
"SIGNER_AUTHORITY_URL": "http://authority:8080",
|
||||
},
|
||||
},
|
||||
"excititor": {
|
||||
"ports": ["8086:8080"],
|
||||
"depends_on": ["postgres", "concelier"],
|
||||
"environment": {
|
||||
"EXCITITOR_DB_CONNECTION": "Host=postgres;Database=excititor;Username=stellaops;Password=${POSTGRES_PASSWORD}",
|
||||
},
|
||||
},
|
||||
"gateway": {
|
||||
"ports": ["8000:8080"],
|
||||
"depends_on": ["authority"],
|
||||
"environment": {
|
||||
"GATEWAY_AUTHORITY_URL": "http://authority:8080",
|
||||
},
|
||||
},
|
||||
"scheduler": {
|
||||
"ports": ["8087:8080"],
|
||||
"depends_on": ["postgres", "valkey"],
|
||||
"environment": {
|
||||
"SCHEDULER_DB_CONNECTION": "Host=postgres;Database=scheduler;Username=stellaops;Password=${POSTGRES_PASSWORD}",
|
||||
"SCHEDULER_QUEUE_URL": "valkey:6379",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# Infrastructure services
|
||||
INFRASTRUCTURE_SERVICES = {
|
||||
"postgres": {
|
||||
"image": "postgres:16-alpine",
|
||||
"environment": {
|
||||
"POSTGRES_USER": "stellaops",
|
||||
"POSTGRES_PASSWORD": "${POSTGRES_PASSWORD:-stellaops}",
|
||||
"POSTGRES_DB": "stellaops",
|
||||
},
|
||||
"volumes": ["postgres_data:/var/lib/postgresql/data"],
|
||||
"healthcheck": {
|
||||
"test": ["CMD-SHELL", "pg_isready -U stellaops"],
|
||||
"interval": "10s",
|
||||
"timeout": "5s",
|
||||
"retries": 5,
|
||||
},
|
||||
},
|
||||
"valkey": {
|
||||
"image": "valkey/valkey:8-alpine",
|
||||
"volumes": ["valkey_data:/data"],
|
||||
"healthcheck": {
|
||||
"test": ["CMD", "valkey-cli", "ping"],
|
||||
"interval": "10s",
|
||||
"timeout": "5s",
|
||||
"retries": 5,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def read_service_versions() -> Dict[str, dict]:
|
||||
"""Read service versions from manifest."""
|
||||
if not MANIFEST_FILE.exists():
|
||||
return {}
|
||||
|
||||
try:
|
||||
manifest = json.loads(MANIFEST_FILE.read_text(encoding="utf-8"))
|
||||
return manifest.get("services", {})
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
|
||||
|
||||
def generate_compose(
|
||||
version: str,
|
||||
codename: str,
|
||||
registry: str,
|
||||
services: Dict[str, dict],
|
||||
airgap: bool = False,
|
||||
include_deps: bool = True,
|
||||
) -> str:
|
||||
"""Generate Docker Compose YAML."""
|
||||
now = datetime.now(timezone.utc)
|
||||
|
||||
lines = [
|
||||
"# Docker Compose for StellaOps Suite",
|
||||
f"# Version: {version} \"{codename}\"",
|
||||
f"# Generated: {now.isoformat()}",
|
||||
"#",
|
||||
"# Usage:",
|
||||
"# docker compose up -d",
|
||||
"# docker compose logs -f",
|
||||
"# docker compose down",
|
||||
"#",
|
||||
"# Environment variables:",
|
||||
"# POSTGRES_PASSWORD - PostgreSQL password (default: stellaops)",
|
||||
"#",
|
||||
"",
|
||||
"services:",
|
||||
]
|
||||
|
||||
# Add infrastructure services if requested
|
||||
if include_deps:
|
||||
for name, config in INFRASTRUCTURE_SERVICES.items():
|
||||
lines.extend(generate_service_block(name, config, indent=2))
|
||||
|
||||
# Add StellaOps services
|
||||
for svc_name, svc_def in SERVICE_DEFINITIONS.items():
|
||||
# Get version info from manifest
|
||||
manifest_info = services.get(svc_name, {})
|
||||
docker_tag = manifest_info.get("dockerTag") or manifest_info.get("version", version)
|
||||
|
||||
# Build image reference
|
||||
if airgap:
|
||||
image = f"localhost:5000/{svc_name}:{docker_tag}"
|
||||
else:
|
||||
image = f"{registry}/{svc_name}:{docker_tag}"
|
||||
|
||||
# Build service config
|
||||
config = {
|
||||
"image": image,
|
||||
"restart": "unless-stopped",
|
||||
**svc_def,
|
||||
}
|
||||
|
||||
# Add release labels
|
||||
config["labels"] = {
|
||||
"com.stellaops.release.version": version,
|
||||
"com.stellaops.release.codename": codename,
|
||||
"com.stellaops.service.name": svc_name,
|
||||
"com.stellaops.service.version": manifest_info.get("version", "1.0.0"),
|
||||
}
|
||||
|
||||
lines.extend(generate_service_block(svc_name, config, indent=2))
|
||||
|
||||
# Add volumes
|
||||
lines.extend([
|
||||
"",
|
||||
"volumes:",
|
||||
])
|
||||
|
||||
if include_deps:
|
||||
lines.extend([
|
||||
" postgres_data:",
|
||||
" driver: local",
|
||||
" valkey_data:",
|
||||
" driver: local",
|
||||
])
|
||||
|
||||
# Add networks
|
||||
lines.extend([
|
||||
"",
|
||||
"networks:",
|
||||
" default:",
|
||||
" name: stellaops",
|
||||
" driver: bridge",
|
||||
])
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def generate_service_block(name: str, config: dict, indent: int = 2) -> List[str]:
|
||||
"""Generate YAML block for a service."""
|
||||
prefix = " " * indent
|
||||
lines = [
|
||||
"",
|
||||
f"{prefix}{name}:",
|
||||
]
|
||||
|
||||
inner_prefix = " " * (indent + 2)
|
||||
|
||||
# Image
|
||||
if "image" in config:
|
||||
lines.append(f"{inner_prefix}image: {config['image']}")
|
||||
|
||||
# Container name
|
||||
lines.append(f"{inner_prefix}container_name: stellaops-{name}")
|
||||
|
||||
# Restart policy
|
||||
if "restart" in config:
|
||||
lines.append(f"{inner_prefix}restart: {config['restart']}")
|
||||
|
||||
# Ports
|
||||
if "ports" in config:
|
||||
lines.append(f"{inner_prefix}ports:")
|
||||
for port in config["ports"]:
|
||||
lines.append(f"{inner_prefix} - \"{port}\"")
|
||||
|
||||
# Volumes
|
||||
if "volumes" in config:
|
||||
lines.append(f"{inner_prefix}volumes:")
|
||||
for vol in config["volumes"]:
|
||||
lines.append(f"{inner_prefix} - {vol}")
|
||||
|
||||
# Environment
|
||||
if "environment" in config:
|
||||
lines.append(f"{inner_prefix}environment:")
|
||||
for key, value in config["environment"].items():
|
||||
lines.append(f"{inner_prefix} {key}: \"{value}\"")
|
||||
|
||||
# Depends on
|
||||
if "depends_on" in config:
|
||||
lines.append(f"{inner_prefix}depends_on:")
|
||||
for dep in config["depends_on"]:
|
||||
lines.append(f"{inner_prefix} {dep}:")
|
||||
lines.append(f"{inner_prefix} condition: service_healthy")
|
||||
|
||||
# Health check
|
||||
if "healthcheck" in config:
|
||||
hc = config["healthcheck"]
|
||||
lines.append(f"{inner_prefix}healthcheck:")
|
||||
if "test" in hc:
|
||||
test = hc["test"]
|
||||
if isinstance(test, list):
|
||||
lines.append(f"{inner_prefix} test: {json.dumps(test)}")
|
||||
else:
|
||||
lines.append(f"{inner_prefix} test: \"{test}\"")
|
||||
for key in ["interval", "timeout", "retries", "start_period"]:
|
||||
if key in hc:
|
||||
lines.append(f"{inner_prefix} {key}: {hc[key]}")
|
||||
|
||||
# Labels
|
||||
if "labels" in config:
|
||||
lines.append(f"{inner_prefix}labels:")
|
||||
for key, value in config["labels"].items():
|
||||
lines.append(f"{inner_prefix} {key}: \"{value}\"")
|
||||
|
||||
return lines
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate pinned Docker Compose files for suite releases",
|
||||
)
|
||||
|
||||
parser.add_argument("version", help="Suite version (YYYY.MM format)")
|
||||
parser.add_argument("codename", help="Release codename")
|
||||
parser.add_argument("--output", "-o", help="Output file")
|
||||
parser.add_argument(
|
||||
"--airgap",
|
||||
action="store_true",
|
||||
help="Generate air-gap variant (localhost:5000 registry)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--registry",
|
||||
default=DEFAULT_REGISTRY,
|
||||
help="Container registry URL",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--include-deps",
|
||||
action="store_true",
|
||||
default=True,
|
||||
help="Include infrastructure dependencies",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-deps",
|
||||
action="store_true",
|
||||
help="Exclude infrastructure dependencies",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Read service versions
|
||||
services = read_service_versions()
|
||||
if not services:
|
||||
print("Warning: No service versions found in manifest", file=sys.stderr)
|
||||
|
||||
# Generate compose file
|
||||
include_deps = args.include_deps and not args.no_deps
|
||||
compose = generate_compose(
|
||||
version=args.version,
|
||||
codename=args.codename,
|
||||
registry=args.registry,
|
||||
services=services,
|
||||
airgap=args.airgap,
|
||||
include_deps=include_deps,
|
||||
)
|
||||
|
||||
# Output
|
||||
if args.output:
|
||||
Path(args.output).write_text(compose, encoding="utf-8")
|
||||
print(f"Docker Compose written to: {args.output}", file=sys.stderr)
|
||||
else:
|
||||
print(compose)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user