import logging from collections.abc import Iterator from fastapi import FastAPI, HTTPException, Request, status from fastapi.responses import StreamingResponse from app.auth import verify_build_token from app.builder import BuildError, cleanup_build, prepare_build, stream_build_and_push from app.models import parse_json_build, parse_multipart_build logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") app = FastAPI(title="Docker Build Service", version="0.1.0") @app.get("/health") def health() -> dict[str, str]: return {"status": "ok"} @app.post("/build") async def build(request: Request) -> StreamingResponse: content_type = request.headers.get("content-type", "") if content_type.startswith("multipart/form-data"): build_request = await parse_multipart_build(request) elif content_type.startswith("application/json"): build_request = await parse_json_build(request) else: raise HTTPException( status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, detail="Content-Type must be application/json or multipart/form-data", ) claims = verify_build_token(build_request.token) uploads = ( build_request.uploads if build_request.uploads and claims.uploads_authorized() else None ) prepared = prepare_build( claims, commit=build_request.commit, uploads=uploads, repo_username=build_request.repo_username, repo_password=build_request.repo_password, ) def generate() -> Iterator[str]: try: yield from stream_build_and_push( prepared, tags=build_request.tags, remote=build_request.remote, username=build_request.username, password=build_request.password, ) except BuildError as exc: # Headers are already sent as 200 once streaming starts. yield f"\n[build-service] ERROR: {exc.message}\n" finally: cleanup_build(prepared) return StreamingResponse(generate(), media_type="text/plain; charset=utf-8")