Coverage for gco / services / api_routes / templates.py: 93%
90 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-30 21:47 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-30 21:47 +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)
21if TYPE_CHECKING:
22 from gco.services.template_store import TemplateStore
24router = APIRouter(tags=["Templates"])
25logger = logging.getLogger(__name__)
28def _get_template_store() -> TemplateStore:
29 from gco.services.manifest_api import template_store
31 if template_store is None:
32 raise HTTPException(status_code=503, detail="Template store not initialized")
33 return template_store
36@router.get("/api/v1/templates")
37async def list_templates() -> Response:
38 """List all job templates."""
39 store = _get_template_store()
40 try:
41 templates = store.list_templates()
42 return JSONResponse(
43 status_code=200,
44 content={
45 "timestamp": datetime.now(UTC).isoformat(),
46 "count": len(templates),
47 "templates": templates,
48 },
49 )
50 except Exception as e:
51 logger.error(f"Failed to list templates: {e}")
52 raise HTTPException(status_code=500, detail=f"Failed to list templates: {e!s}") from e
55@router.post("/api/v1/templates")
56async def create_template(request: JobTemplateRequest) -> Response:
57 """Create a new job template."""
58 store = _get_template_store()
59 try:
60 template = store.create_template(
61 name=request.name,
62 manifest=request.manifest,
63 description=request.description,
64 parameters=request.parameters,
65 )
66 return JSONResponse(
67 status_code=201,
68 content={
69 "timestamp": datetime.now(UTC).isoformat(),
70 "message": "Template created successfully",
71 "template": template,
72 },
73 )
74 except ValueError as e:
75 raise HTTPException(status_code=409, detail=str(e)) from e
76 except Exception as e:
77 logger.error(f"Failed to create template: {e}")
78 raise HTTPException(status_code=500, detail=f"Failed to create template: {e!s}") from e
81@router.get("/api/v1/templates/{name}")
82async def get_template(name: str) -> Response:
83 """Get a specific job template."""
84 store = _get_template_store()
85 try:
86 template = store.get_template(name)
87 if template is None:
88 raise HTTPException(status_code=404, detail=f"Template '{name}' not found")
89 return JSONResponse(
90 status_code=200,
91 content={"timestamp": datetime.now(UTC).isoformat(), "template": template},
92 )
93 except HTTPException:
94 raise
95 except Exception as e:
96 logger.error(f"Failed to get template: {e}")
97 raise HTTPException(status_code=500, detail=f"Failed to get template: {e!s}") from e
100@router.delete("/api/v1/templates/{name}")
101async def delete_template(name: str) -> Response:
102 """Delete a job template."""
103 store = _get_template_store()
104 try:
105 deleted = store.delete_template(name)
106 if not deleted:
107 raise HTTPException(status_code=404, detail=f"Template '{name}' not found")
108 return JSONResponse(
109 status_code=200,
110 content={
111 "timestamp": datetime.now(UTC).isoformat(),
112 "message": f"Template '{name}' deleted successfully",
113 },
114 )
115 except HTTPException:
116 raise
117 except Exception as e:
118 logger.error(f"Failed to delete template: {e}")
119 raise HTTPException(status_code=500, detail=f"Failed to delete template: {e!s}") from e
122@router.post("/api/v1/jobs/from-template/{name}")
123async def create_job_from_template(name: str, request: JobFromTemplateRequest) -> Response:
124 """Create a job from a template with parameter substitution."""
125 processor = _check_processor()
126 store = _get_template_store()
128 template = store.get_template(name)
129 if template is None:
130 raise HTTPException(status_code=404, detail=f"Template '{name}' not found")
132 _check_namespace(request.namespace, processor)
134 try:
135 parameters = {**template.get("parameters", {}), **(request.parameters or {})}
136 parameters["name"] = request.name
138 manifest = _apply_template_parameters(template["manifest"], parameters)
140 if "metadata" not in manifest: 140 ↛ 141line 140 didn't jump to line 141 because the condition on line 140 was never true
141 manifest["metadata"] = {}
142 manifest["metadata"]["namespace"] = request.namespace
143 manifest["metadata"]["name"] = request.name
145 if "labels" not in manifest["metadata"]: 145 ↛ 147line 145 didn't jump to line 147 because the condition on line 145 was always true
146 manifest["metadata"]["labels"] = {}
147 manifest["metadata"]["labels"]["gco.io/template"] = name
149 submission_request = ManifestSubmissionRequest(
150 manifests=[manifest], namespace=request.namespace, dry_run=False, validate=True
151 )
153 result = await processor.process_manifest_submission(submission_request)
155 response = {
156 "cluster_id": processor.cluster_id,
157 "region": processor.region,
158 "timestamp": datetime.now(UTC).isoformat(),
159 "template": name,
160 "job_name": request.name,
161 "namespace": request.namespace,
162 "success": result.success,
163 "parameters_applied": parameters,
164 "errors": result.errors,
165 }
167 status_code = 201 if result.success else 400
168 return JSONResponse(status_code=status_code, content=response)
170 except HTTPException:
171 raise
172 except Exception as e:
173 logger.error(f"Error creating job from template: {e}")
174 raise HTTPException(status_code=500, detail=f"Internal server error: {e!s}") from e