Coverage for gco / services / api_routes / templates.py: 100.00%
89 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-09-14 22:07 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-09-14 22:07 +0000
1"""Job template CRUD and job-from-template endpoints."""
3from __future__ import annotations
5import logging
6from datetime import UTC, datetime
7from typing import TYPE_CHECKING
9from fastapi import APIRouter, HTTPException
10from fastapi.responses import JSONResponse, Response
12from gco.models import ManifestSubmissionRequest
13from gco.services.api_shared import (
14 JobFromTemplateRequest,
15 JobTemplateRequest,
16 _apply_template_parameters,
17 _check_namespace,
18 _check_processor,
19 internal_server_error,
20)
22if TYPE_CHECKING:
23 from gco.services.template_store import TemplateStore
25router = APIRouter(tags=["Templates"])
26logger = logging.getLogger(__name__)
29def _get_template_store() -> TemplateStore:
30 from gco.services.manifest_api import template_store
32 if template_store is None:
33 raise HTTPException(status_code=503, detail="Template store not initialized")
34 return template_store
37@router.get("/api/v1/templates")
38async def list_templates() -> Response:
39 """List all job templates."""
40 store = _get_template_store()
41 try:
42 templates = store.list_templates()
43 return JSONResponse(
44 status_code=200,
45 content={
46 "timestamp": datetime.now(UTC).isoformat(),
47 "count": len(templates),
48 "templates": templates,
49 },
50 )
51 except Exception as e:
52 logger.error(f"Failed to list templates: {e}")
53 raise HTTPException(status_code=500, detail=f"Failed to list templates: {e!s}") from e
56@router.post("/api/v1/templates")
57async def create_template(request: JobTemplateRequest) -> Response:
58 """Create a new job template."""
59 store = _get_template_store()
60 try:
61 template = store.create_template(
62 name=request.name,
63 manifest=request.manifest,
64 description=request.description,
65 parameters=request.parameters,
66 )
67 return JSONResponse(
68 status_code=201,
69 content={
70 "timestamp": datetime.now(UTC).isoformat(),
71 "message": "Template created successfully",
72 "template": template,
73 },
74 )
75 except ValueError as e:
76 raise HTTPException(status_code=409, detail=str(e)) from e
77 except Exception as e:
78 logger.error(f"Failed to create template: {e}")
79 raise HTTPException(status_code=500, detail=f"Failed to create template: {e!s}") from e
82@router.get("/api/v1/templates/{name}")
83async def get_template(name: str) -> Response:
84 """Get a specific job template."""
85 store = _get_template_store()
86 try:
87 template = store.get_template(name)
88 if template is None:
89 raise HTTPException(status_code=404, detail=f"Template '{name}' not found")
90 return JSONResponse(
91 status_code=200,
92 content={"timestamp": datetime.now(UTC).isoformat(), "template": template},
93 )
94 except HTTPException:
95 raise
96 except Exception as e:
97 logger.error(f"Failed to get template: {e}")
98 raise HTTPException(status_code=500, detail=f"Failed to get template: {e!s}") from e
101@router.delete("/api/v1/templates/{name}")
102async def delete_template(name: str) -> Response:
103 """Delete a job template."""
104 store = _get_template_store()
105 try:
106 deleted = store.delete_template(name)
107 if not deleted:
108 raise HTTPException(status_code=404, detail=f"Template '{name}' not found")
109 return JSONResponse(
110 status_code=200,
111 content={
112 "timestamp": datetime.now(UTC).isoformat(),
113 "message": f"Template '{name}' deleted successfully",
114 },
115 )
116 except HTTPException:
117 raise
118 except Exception as e:
119 logger.error(f"Failed to delete template: {e}")
120 raise HTTPException(status_code=500, detail=f"Failed to delete template: {e!s}") from e
123@router.post("/api/v1/jobs/from-template/{name}")
124async def create_job_from_template(name: str, request: JobFromTemplateRequest) -> Response:
125 """Create a job from a template with parameter substitution."""
126 processor = _check_processor()
127 store = _get_template_store()
129 template = store.get_template(name)
130 if template is None:
131 raise HTTPException(status_code=404, detail=f"Template '{name}' not found")
133 _check_namespace(request.namespace, processor)
135 try:
136 parameters = {**template.get("parameters", {}), **(request.parameters or {})}
137 parameters["name"] = request.name
139 manifest = _apply_template_parameters(template["manifest"], parameters)
141 if "metadata" not in manifest:
142 manifest["metadata"] = {}
143 manifest["metadata"]["namespace"] = request.namespace
144 manifest["metadata"]["name"] = request.name
146 if "labels" not in manifest["metadata"]:
147 manifest["metadata"]["labels"] = {}
148 manifest["metadata"]["labels"]["gco.io/template"] = name
150 submission_request = ManifestSubmissionRequest(
151 manifests=[manifest], namespace=request.namespace, dry_run=False, validate=True
152 )
154 result = await processor.process_manifest_submission(submission_request)
156 response = {
157 "cluster_id": processor.cluster_id,
158 "region": processor.region,
159 "timestamp": datetime.now(UTC).isoformat(),
160 "template": name,
161 "job_name": request.name,
162 "namespace": request.namespace,
163 "success": result.success,
164 "parameters_applied": parameters,
165 "errors": result.errors,
166 }
168 status_code = 201 if result.success else 400
169 return JSONResponse(status_code=status_code, content=response)
171 except HTTPException:
172 raise
173 except Exception as e:
174 raise internal_server_error("creating job from template", e) from e