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
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-30 21:47 +0000
1"""Job template commands."""
3import sys
4from typing import Any
6import click
8from ..config import GCOConfig
9from ..output import get_output_formatter
11pass_config = click.make_pass_decorator(GCOConfig, ensure=True)
14@click.group()
15@pass_config
16def templates(config: Any) -> None:
17 """Manage job templates.
19 Templates are reusable job configurations stored in DynamoDB.
20 They support parameter substitution using {{parameter}} syntax.
21 """
22 pass
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.
31 Examples:
32 gco templates list
33 """
34 formatter = get_output_formatter(config)
36 try:
37 from ..aws_client import get_aws_client
39 aws_client = get_aws_client(config)
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 )
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
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)
66 except Exception as e:
67 formatter.print_error(f"Failed to list templates: {e}")
68 sys.exit(1)
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.
78 Examples:
79 gco templates get gpu-training-template
80 """
81 formatter = get_output_formatter(config)
83 try:
84 from ..aws_client import get_aws_client
86 aws_client = get_aws_client(config)
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 )
95 template = result.get("template", {})
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')}")
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}")
110 print("\n Manifest:")
111 import json
113 print(json.dumps(template.get("manifest", {}), indent=4))
114 else:
115 formatter.print(result)
117 except Exception as e:
118 formatter.print_error(f"Failed to get template: {e}")
119 sys.exit(1)
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.
134 The manifest can contain {{parameter}} placeholders that will be
135 substituted when creating jobs from the template.
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
143 formatter = get_output_formatter(config)
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
152 try:
153 # Load manifest
154 with open(manifest_path, encoding="utf-8") as f:
155 manifest = yaml.safe_load(f)
157 from ..aws_client import get_aws_client
159 aws_client = get_aws_client(config)
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 )
174 formatter.print_success(f"Template '{name}' created successfully")
175 formatter.print(result)
177 except Exception as e:
178 formatter.print_error(f"Failed to create template: {e}")
179 sys.exit(1)
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.
190 Examples:
191 gco templates delete old-template
192 gco templates delete old-template -y
193 """
194 formatter = get_output_formatter(config)
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)
199 try:
200 from ..aws_client import get_aws_client
202 aws_client = get_aws_client(config)
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 )
211 formatter.print_success(f"Template '{name}' deleted")
212 formatter.print(result)
214 except Exception as e:
215 formatter.print_error(f"Failed to delete template: {e}")
216 sys.exit(1)
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.
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)
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
244 try:
245 from ..aws_client import get_aws_client
247 aws_client = get_aws_client(config)
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 )
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")
265 formatter.print(result)
267 except Exception as e:
268 formatter.print_error(f"Failed to run template: {e}")
269 sys.exit(1)