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

1"""Job template CRUD and job-from-template endpoints.""" 

2 

3from __future__ import annotations 

4 

5import logging 

6from datetime import UTC, datetime 

7from typing import TYPE_CHECKING 

8 

9from fastapi import APIRouter, HTTPException 

10from fastapi.responses import JSONResponse, Response 

11 

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) 

20 

21if TYPE_CHECKING: 

22 from gco.services.template_store import TemplateStore 

23 

24router = APIRouter(tags=["Templates"]) 

25logger = logging.getLogger(__name__) 

26 

27 

28def _get_template_store() -> TemplateStore: 

29 from gco.services.manifest_api import template_store 

30 

31 if template_store is None: 

32 raise HTTPException(status_code=503, detail="Template store not initialized") 

33 return template_store 

34 

35 

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 

53 

54 

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 

79 

80 

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 

98 

99 

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 

120 

121 

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() 

127 

128 template = store.get_template(name) 

129 if template is None: 

130 raise HTTPException(status_code=404, detail=f"Template '{name}' not found") 

131 

132 _check_namespace(request.namespace, processor) 

133 

134 try: 

135 parameters = {**template.get("parameters", {}), **(request.parameters or {})} 

136 parameters["name"] = request.name 

137 

138 manifest = _apply_template_parameters(template["manifest"], parameters) 

139 

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 

144 

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 

148 

149 submission_request = ManifestSubmissionRequest( 

150 manifests=[manifest], namespace=request.namespace, dry_run=False, validate=True 

151 ) 

152 

153 result = await processor.process_manifest_submission(submission_request) 

154 

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 } 

166 

167 status_code = 201 if result.success else 400 

168 return JSONResponse(status_code=status_code, content=response) 

169 

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