feat(01-05): add build orchestration service
- Implement BuildService for coordinating ISO build lifecycle - Integrate sandbox and deterministic config for reproducible builds - Add cache lookup before build execution (same hash = return cached) - Handle build status transitions: pending -> building -> completed/failed
This commit is contained in:
parent
683a1efcf5
commit
c01b4cbf54
2 changed files with 148 additions and 1 deletions
|
|
@ -1,5 +1,7 @@
|
||||||
"""Services package for business logic."""
|
"""Services package for business logic."""
|
||||||
|
|
||||||
|
from backend.app.services.build import BuildService
|
||||||
|
from backend.app.services.deterministic import DeterministicBuildConfig
|
||||||
from backend.app.services.sandbox import BuildSandbox, SandboxConfig
|
from backend.app.services.sandbox import BuildSandbox, SandboxConfig
|
||||||
|
|
||||||
__all__ = ["BuildSandbox", "SandboxConfig"]
|
__all__ = ["BuildSandbox", "SandboxConfig", "DeterministicBuildConfig", "BuildService"]
|
||||||
|
|
|
||||||
145
backend/app/services/build.py
Normal file
145
backend/app/services/build.py
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
"""
|
||||||
|
Build orchestration service.
|
||||||
|
|
||||||
|
Coordinates:
|
||||||
|
1. Configuration validation
|
||||||
|
2. Hash computation (for caching)
|
||||||
|
3. Sandbox creation
|
||||||
|
4. Build execution
|
||||||
|
5. Result storage
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import UTC, datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from backend.app.core.config import settings
|
||||||
|
from backend.app.db.models.build import Build, BuildStatus
|
||||||
|
from backend.app.services.deterministic import DeterministicBuildConfig
|
||||||
|
from backend.app.services.sandbox import BuildSandbox
|
||||||
|
|
||||||
|
|
||||||
|
class BuildService:
|
||||||
|
"""Orchestrates ISO build process."""
|
||||||
|
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
self.sandbox = BuildSandbox()
|
||||||
|
self.output_root = Path(settings.iso_output_root)
|
||||||
|
|
||||||
|
async def get_or_create_build(
|
||||||
|
self,
|
||||||
|
config: dict[str, Any],
|
||||||
|
) -> tuple[Build, bool]:
|
||||||
|
"""
|
||||||
|
Get existing build from cache or create new one.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (Build, is_cached)
|
||||||
|
"""
|
||||||
|
# Compute deterministic hash
|
||||||
|
config_hash = DeterministicBuildConfig.compute_config_hash(config)
|
||||||
|
|
||||||
|
# Check cache
|
||||||
|
stmt = select(Build).where(
|
||||||
|
Build.config_hash == config_hash,
|
||||||
|
Build.status == BuildStatus.COMPLETED,
|
||||||
|
)
|
||||||
|
result = await self.db.execute(stmt)
|
||||||
|
cached_build = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if cached_build:
|
||||||
|
# Return cached build
|
||||||
|
return cached_build, True
|
||||||
|
|
||||||
|
# Create new build
|
||||||
|
build = Build(
|
||||||
|
id=uuid4(),
|
||||||
|
config_hash=config_hash,
|
||||||
|
status=BuildStatus.PENDING,
|
||||||
|
)
|
||||||
|
self.db.add(build)
|
||||||
|
await self.db.commit()
|
||||||
|
await self.db.refresh(build)
|
||||||
|
|
||||||
|
return build, False
|
||||||
|
|
||||||
|
async def execute_build(
|
||||||
|
self,
|
||||||
|
build: Build,
|
||||||
|
config: dict[str, Any],
|
||||||
|
) -> Build:
|
||||||
|
"""
|
||||||
|
Execute the actual ISO build.
|
||||||
|
|
||||||
|
Process:
|
||||||
|
1. Update status to building
|
||||||
|
2. Create sandbox container
|
||||||
|
3. Generate archiso profile
|
||||||
|
4. Run build
|
||||||
|
5. Update status with result
|
||||||
|
"""
|
||||||
|
build.status = BuildStatus.BUILDING
|
||||||
|
build.started_at = datetime.now(UTC)
|
||||||
|
await self.db.commit()
|
||||||
|
|
||||||
|
container_path = None
|
||||||
|
profile_path = self.output_root / str(build.id) / "profile"
|
||||||
|
output_path = self.output_root / str(build.id) / "output"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create sandbox
|
||||||
|
container_path = await self.sandbox.create_build_container(str(build.id))
|
||||||
|
|
||||||
|
# Generate deterministic profile
|
||||||
|
source_date_epoch = DeterministicBuildConfig.get_source_date_epoch(
|
||||||
|
build.config_hash
|
||||||
|
)
|
||||||
|
DeterministicBuildConfig.create_archiso_profile(
|
||||||
|
config, profile_path, source_date_epoch
|
||||||
|
)
|
||||||
|
|
||||||
|
# Run build in sandbox
|
||||||
|
return_code, stdout, stderr = await self.sandbox.run_build(
|
||||||
|
container_path, profile_path, output_path, source_date_epoch
|
||||||
|
)
|
||||||
|
|
||||||
|
if return_code == 0:
|
||||||
|
# Find generated ISO
|
||||||
|
iso_files = list(output_path.glob("*.iso"))
|
||||||
|
if iso_files:
|
||||||
|
build.iso_path = str(iso_files[0])
|
||||||
|
build.status = BuildStatus.COMPLETED
|
||||||
|
else:
|
||||||
|
build.status = BuildStatus.FAILED
|
||||||
|
build.error_message = "Build completed but no ISO found"
|
||||||
|
else:
|
||||||
|
build.status = BuildStatus.FAILED
|
||||||
|
build.error_message = stderr or f"Build failed with code {return_code}"
|
||||||
|
|
||||||
|
build.build_log = stdout + "\n" + stderr
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
build.status = BuildStatus.FAILED
|
||||||
|
build.error_message = str(e)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Cleanup sandbox
|
||||||
|
if container_path:
|
||||||
|
await self.sandbox.cleanup_container(container_path)
|
||||||
|
|
||||||
|
build.completed_at = datetime.now(UTC)
|
||||||
|
await self.db.commit()
|
||||||
|
await self.db.refresh(build)
|
||||||
|
|
||||||
|
return build
|
||||||
|
|
||||||
|
async def get_build_status(self, build_id: str) -> Build | None:
|
||||||
|
"""Get build by ID."""
|
||||||
|
stmt = select(Build).where(Build.id == build_id)
|
||||||
|
result = await self.db.execute(stmt)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
Loading…
Add table
Reference in a new issue