Coverage for cli / commands / templates_cmd.py: 94%

139 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-30 21:47 +0000

1"""Job template commands.""" 

2 

3import sys 

4from typing import Any 

5 

6import click 

7 

8from ..config import GCOConfig 

9from ..output import get_output_formatter 

10 

11pass_config = click.make_pass_decorator(GCOConfig, ensure=True) 

12 

13 

14@click.group() 

15@pass_config 

16def templates(config: Any) -> None: 

17 """Manage job templates. 

18 

19 Templates are reusable job configurations stored in DynamoDB. 

20 They support parameter substitution using {{parameter}} syntax. 

21 """ 

22 pass 

23 

24 

25@templates.command("list") 

26@click.option("--region", "-r", help="Region to query (any region works)") 

27@pass_config 

28def templates_list(config: Any, region: Any) -> None: 

29 """List all job templates. 

30 

31 Examples: 

32 gco templates list 

33 """ 

34 formatter = get_output_formatter(config) 

35 

36 try: 

37 from ..aws_client import get_aws_client 

38 

39 aws_client = get_aws_client(config) 

40 

41 query_region = region or config.default_region 

42 result = aws_client.call_api( 

43 method="GET", 

44 path="/api/v1/templates", 

45 region=query_region, 

46 ) 

47 

48 if config.output_format == "table": 48 ↛ 64line 48 didn't jump to line 64 because the condition on line 48 was always true

49 templates_data = result.get("templates", []) 

50 if not templates_data: 

51 formatter.print_info("No templates found") 

52 return 

53 

54 print(f"\n Job Templates ({result.get('count', 0)} total)") 

55 print(" " + "-" * 70) 

56 print(" NAME DESCRIPTION CREATED") 

57 print(" " + "-" * 70) 

58 for t in templates_data: 

59 name = t.get("name", "")[:28] 

60 desc = (t.get("description") or "")[:28] 

61 created = (t.get("created_at") or "")[:19] 

62 print(f" {name:<30} {desc:<30} {created}") 

63 else: 

64 formatter.print(result) 

65 

66 except Exception as e: 

67 formatter.print_error(f"Failed to list templates: {e}") 

68 sys.exit(1) 

69 

70 

71@templates.command("get") 

72@click.argument("name") 

73@click.option("--region", "-r", help="Region to query (any region works)") 

74@pass_config 

75def templates_get(config: Any, name: Any, region: Any) -> None: 

76 """Get details of a specific template. 

77 

78 Examples: 

79 gco templates get gpu-training-template 

80 """ 

81 formatter = get_output_formatter(config) 

82 

83 try: 

84 from ..aws_client import get_aws_client 

85 

86 aws_client = get_aws_client(config) 

87 

88 query_region = region or config.default_region 

89 result = aws_client.call_api( 

90 method="GET", 

91 path=f"/api/v1/templates/{name}", 

92 region=query_region, 

93 ) 

94 

95 template = result.get("template", {}) 

96 

97 if config.output_format == "table": 97 ↛ 115line 97 didn't jump to line 115 because the condition on line 97 was always true

98 print(f"\n Template: {template.get('name')}") 

99 print(" " + "-" * 50) 

100 print(f" Description: {template.get('description') or 'N/A'}") 

101 print(f" Created: {template.get('created_at')}") 

102 print(f" Updated: {template.get('updated_at')}") 

103 

104 params = template.get("parameters", {}) 

105 if params: 105 ↛ 110line 105 didn't jump to line 110 because the condition on line 105 was always true

106 print("\n Default Parameters:") 

107 for k, v in params.items(): 

108 print(f" {k}: {v}") 

109 

110 print("\n Manifest:") 

111 import json 

112 

113 print(json.dumps(template.get("manifest", {}), indent=4)) 

114 else: 

115 formatter.print(result) 

116 

117 except Exception as e: 

118 formatter.print_error(f"Failed to get template: {e}") 

119 sys.exit(1) 

120 

121 

122@templates.command("create") 

123@click.argument("manifest_path", type=click.Path(exists=True)) 

124@click.option("--name", "-n", required=True, help="Template name") 

125@click.option("--description", "-d", help="Template description") 

126@click.option("--param", "-p", multiple=True, help="Default parameter (key=value)") 

127@click.option("--region", "-r", help="Region to use (any region works)") 

128@pass_config 

129def templates_create( 

130 config: Any, manifest_path: Any, name: Any, description: Any, param: Any, region: Any 

131) -> None: 

132 """Create a new job template from a manifest file. 

133 

134 The manifest can contain {{parameter}} placeholders that will be 

135 substituted when creating jobs from the template. 

136 

137 Examples: 

138 gco templates create job.yaml --name gpu-template -d "GPU training template" 

139 gco templates create job.yaml -n my-template -p image=pytorch:latest -p gpus=4 

140 """ 

141 import yaml 

142 

143 formatter = get_output_formatter(config) 

144 

145 # Parse parameters 

146 parameters = {} 

147 for p in param: 

148 if "=" in p: 148 ↛ 147line 148 didn't jump to line 147 because the condition on line 148 was always true

149 k, v = p.split("=", 1) 

150 parameters[k] = v 

151 

152 try: 

153 # Load manifest 

154 with open(manifest_path, encoding="utf-8") as f: 

155 manifest = yaml.safe_load(f) 

156 

157 from ..aws_client import get_aws_client 

158 

159 aws_client = get_aws_client(config) 

160 

161 query_region = region or config.default_region 

162 result = aws_client.call_api( 

163 method="POST", 

164 path="/api/v1/templates", 

165 region=query_region, 

166 body={ 

167 "name": name, 

168 "description": description, 

169 "manifest": manifest, 

170 "parameters": parameters if parameters else None, 

171 }, 

172 ) 

173 

174 formatter.print_success(f"Template '{name}' created successfully") 

175 formatter.print(result) 

176 

177 except Exception as e: 

178 formatter.print_error(f"Failed to create template: {e}") 

179 sys.exit(1) 

180 

181 

182@templates.command("delete") 

183@click.argument("name") 

184@click.option("--region", "-r", help="Region to use (any region works)") 

185@click.option("--yes", "-y", is_flag=True, help="Skip confirmation") 

186@pass_config 

187def templates_delete(config: Any, name: Any, region: Any, yes: Any) -> None: 

188 """Delete a job template. 

189 

190 Examples: 

191 gco templates delete old-template 

192 gco templates delete old-template -y 

193 """ 

194 formatter = get_output_formatter(config) 

195 

196 if not yes: 196 ↛ 197line 196 didn't jump to line 197 because the condition on line 196 was never true

197 click.confirm(f"Delete template '{name}'?", abort=True) 

198 

199 try: 

200 from ..aws_client import get_aws_client 

201 

202 aws_client = get_aws_client(config) 

203 

204 query_region = region or config.default_region 

205 result = aws_client.call_api( 

206 method="DELETE", 

207 path=f"/api/v1/templates/{name}", 

208 region=query_region, 

209 ) 

210 

211 formatter.print_success(f"Template '{name}' deleted") 

212 formatter.print(result) 

213 

214 except Exception as e: 

215 formatter.print_error(f"Failed to delete template: {e}") 

216 sys.exit(1) 

217 

218 

219@templates.command("run") 

220@click.argument("template_name") 

221@click.option("--name", "-n", required=True, help="Job name") 

222@click.option("--namespace", default="gco-jobs", help="Kubernetes namespace") 

223@click.option("--param", "-p", multiple=True, help="Parameter override (key=value)") 

224@click.option("--region", "-r", required=True, help="Region to run the job") 

225@pass_config 

226def templates_run( 

227 config: Any, template_name: Any, name: Any, namespace: Any, param: Any, region: Any 

228) -> None: 

229 """Create and run a job from a template. 

230 

231 Examples: 

232 gco templates run gpu-template --name my-job --region us-east-1 

233 gco templates run gpu-template -n my-job -r us-east-1 -p image=custom:v1 

234 """ 

235 formatter = get_output_formatter(config) 

236 

237 # Parse parameters 

238 parameters = {} 

239 for p in param: 

240 if "=" in p: 240 ↛ 239line 240 didn't jump to line 239 because the condition on line 240 was always true

241 k, v = p.split("=", 1) 

242 parameters[k] = v 

243 

244 try: 

245 from ..aws_client import get_aws_client 

246 

247 aws_client = get_aws_client(config) 

248 

249 result = aws_client.call_api( 

250 method="POST", 

251 path=f"/api/v1/jobs/from-template/{template_name}", 

252 region=region, 

253 body={ 

254 "name": name, 

255 "namespace": namespace, 

256 "parameters": parameters if parameters else None, 

257 }, 

258 ) 

259 

260 if result.get("success"): 

261 formatter.print_success(f"Job '{name}' created from template '{template_name}'") 

262 else: 

263 formatter.print_error("Failed to create job from template") 

264 

265 formatter.print(result) 

266 

267 except Exception as e: 

268 formatter.print_error(f"Failed to run template: {e}") 

269 sys.exit(1)