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

138 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-10-28 16:19 +0000

1"""Job template commands.""" 

2 

3import sys 

4from typing import Any 

5 

6import click 

7 

8from ..config import GCOConfig 

9from ..output import confirm, 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 

23 

24@templates.command("list") 

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

26@pass_config 

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

28 """List all job templates. 

29 

30 Examples: 

31 gco templates list 

32 """ 

33 formatter = get_output_formatter(config) 

34 

35 try: 

36 from ..aws_client import get_aws_client 

37 

38 aws_client = get_aws_client(config) 

39 

40 query_region = region or (config.default_region if config.use_regional_api else None) 

41 result = aws_client.call_api( 

42 method="GET", 

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

44 region=query_region, 

45 ) 

46 

47 if config.output_format == "table": 

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

49 if not templates_data: 

50 formatter.print_info("No templates found") 

51 return 

52 

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

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

55 print(" NAME DESCRIPTION CREATED") 

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

57 for t in templates_data: 

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

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

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

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

62 else: 

63 formatter.print(result) 

64 

65 except Exception as e: 

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

67 sys.exit(1) 

68 

69 

70@templates.command("get") 

71@click.argument("name") 

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

73@pass_config 

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

75 """Get details of a specific template. 

76 

77 Examples: 

78 gco templates get gpu-training-template 

79 """ 

80 formatter = get_output_formatter(config) 

81 

82 try: 

83 from ..aws_client import get_aws_client 

84 

85 aws_client = get_aws_client(config) 

86 

87 query_region = region or (config.default_region if config.use_regional_api else None) 

88 result = aws_client.call_api( 

89 method="GET", 

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

91 region=query_region, 

92 ) 

93 

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

95 

96 if config.output_format == "table": 

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

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

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

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

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

102 

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

104 if params: 

105 print("\n Default Parameters:") 

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

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

108 

109 print("\n Manifest:") 

110 import json 

111 

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

113 else: 

114 formatter.print(result) 

115 

116 except Exception as e: 

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

118 sys.exit(1) 

119 

120 

121@templates.command("create") 

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

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

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

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

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

127@pass_config 

128def templates_create( 

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

130) -> None: 

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

132 

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

134 substituted when creating jobs from the template. 

135 

136 Examples: 

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

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

139 """ 

140 import yaml 

141 

142 formatter = get_output_formatter(config) 

143 

144 # Parse parameters 

145 parameters = {} 

146 for p in param: 

147 if "=" in p: 

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

149 parameters[k] = v 

150 

151 try: 

152 # Load manifest 

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

154 manifest = yaml.safe_load(f) 

155 

156 from ..aws_client import get_aws_client 

157 

158 aws_client = get_aws_client(config) 

159 

160 query_region = region or (config.default_region if config.use_regional_api else None) 

161 result = aws_client.call_api( 

162 method="POST", 

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

164 region=query_region, 

165 body={ 

166 "name": name, 

167 "description": description, 

168 "manifest": manifest, 

169 "parameters": parameters if parameters else None, 

170 }, 

171 ) 

172 

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

174 formatter.print(result) 

175 

176 except Exception as e: 

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

178 sys.exit(1) 

179 

180 

181@templates.command("delete") 

182@click.argument("name") 

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

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

185@pass_config 

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

187 """Delete a job template. 

188 

189 Examples: 

190 gco templates delete old-template 

191 gco templates delete old-template -y 

192 """ 

193 formatter = get_output_formatter(config) 

194 

195 if not yes: 

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

197 

198 try: 

199 from ..aws_client import get_aws_client 

200 

201 aws_client = get_aws_client(config) 

202 

203 query_region = region or (config.default_region if config.use_regional_api else None) 

204 result = aws_client.call_api( 

205 method="DELETE", 

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

207 region=query_region, 

208 ) 

209 

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

211 formatter.print(result) 

212 

213 except Exception as e: 

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

215 sys.exit(1) 

216 

217 

218@templates.command("run") 

219@click.argument("template_name") 

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

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

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

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

224@pass_config 

225def templates_run( 

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

227) -> None: 

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

229 

230 Examples: 

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

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

233 """ 

234 formatter = get_output_formatter(config) 

235 

236 # Parse parameters 

237 parameters = {} 

238 for p in param: 

239 if "=" in p: 

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

241 parameters[k] = v 

242 

243 try: 

244 from ..aws_client import get_aws_client 

245 

246 aws_client = get_aws_client(config) 

247 

248 result = aws_client.call_api( 

249 method="POST", 

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

251 region=region, 

252 body={ 

253 "name": name, 

254 "namespace": namespace, 

255 "parameters": parameters if parameters else None, 

256 }, 

257 ) 

258 

259 if result.get("success"): 

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

261 else: 

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

263 

264 formatter.print(result) 

265 

266 except Exception as e: 

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

268 sys.exit(1)