Coverage for cli/commands/nodepools_cmd.py: 87.31%
120 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-30 21:22 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-30 21:22 +0000
1"""NodePool 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 nodepools(config: Any) -> None:
17 """Manage Karpenter NodePools with ODCR/Capacity Reservation support."""
18 pass
21@nodepools.command("create-odcr")
22@click.option("--name", "-n", required=True, help="NodePool name")
23@click.option("--region", "-r", required=True, help="AWS region")
24@click.option(
25 "--capacity-reservation-id",
26 "-c",
27 required=True,
28 help="EC2 Capacity Reservation ID (cr-xxx) or ODCR group ARN",
29)
30@click.option(
31 "--instance-type",
32 "-i",
33 multiple=True,
34 help="Instance types (can specify multiple)",
35)
36@click.option("--max-nodes", default=100, help="Maximum nodes in pool")
37@click.option("--fallback-on-demand", is_flag=True, help="Fall back to on-demand if ODCR exhausted")
38@click.option(
39 "--efa",
40 is_flag=True,
41 help="Enable EFA support (adds EFA taint and labels for p4d/p5 instances)",
42)
43@click.option("--output-file", "-o", help="Output manifest to file instead of applying")
44@pass_config
45def create_odcr_nodepool(
46 config: Any,
47 name: Any,
48 region: Any,
49 capacity_reservation_id: Any,
50 instance_type: Any,
51 max_nodes: Any,
52 fallback_on_demand: Any,
53 efa: Any,
54 output_file: Any,
55) -> None:
56 """Create a NodePool backed by an On-Demand Capacity Reservation (ODCR).
58 This creates a Karpenter NodePool and EC2NodeClass configured to use
59 a specific capacity reservation for guaranteed capacity.
61 Examples:
62 gco nodepools create-odcr -n gpu-reserved -r us-east-1 \\
63 -c cr-0123456789abcdef0 -i p4d.24xlarge
65 gco nodepools create-odcr -n ml-training -r us-west-2 \\
66 -c cr-0123456789abcdef0 -i p5.48xlarge --fallback-on-demand
68 gco nodepools create-odcr -n efa-training -r us-east-1 \\
69 -c cr-0123456789abcdef0 -i p4d.24xlarge --efa
71 See: https://karpenter.sh/docs/tasks/odcrs/
72 """
73 from ..nodepools import generate_odcr_nodepool_manifest
75 formatter = get_output_formatter(config)
77 try:
78 manifest = generate_odcr_nodepool_manifest(
79 name=name,
80 region=region,
81 capacity_reservation_id=capacity_reservation_id,
82 instance_types=list(instance_type) if instance_type else None,
83 max_nodes=max_nodes,
84 fallback_on_demand=fallback_on_demand,
85 efa=efa,
86 project_name=config.project_name,
87 )
89 if output_file:
90 with open(output_file, "w", encoding="utf-8") as f:
91 f.write(manifest)
92 formatter.print_success(f"NodePool manifest written to {output_file}")
93 formatter.print_info(f"Apply with: kubectl apply -f {output_file}")
94 else:
95 # Print the manifest for review
96 print(manifest)
97 formatter.print_info("\nTo apply this manifest, save it to a file and run:")
98 formatter.print_info(" kubectl apply -f <filename>.yaml")
100 except Exception as e:
101 formatter.print_error(f"Failed to create ODCR NodePool: {e}")
102 sys.exit(1)
105@nodepools.command("create-capacity-block")
106@click.option("--name", "-n", required=True, help="NodePool name")
107@click.option("--region", "-r", required=True, help="AWS region")
108@click.option(
109 "--capacity-reservation-id",
110 "-c",
111 required=True,
112 help="Capacity Reservation ID (cr-xxx) of the purchased Capacity Block",
113)
114@click.option(
115 "--instance-type",
116 "-i",
117 multiple=True,
118 help="Instance types (can specify multiple)",
119)
120@click.option("--max-nodes", default=100, help="Maximum nodes in pool")
121@click.option(
122 "--fallback-on-demand",
123 is_flag=True,
124 help="Fall back to on-demand when the block is exhausted/expired",
125)
126@click.option(
127 "--efa",
128 is_flag=True,
129 help="Enable EFA support (adds EFA taint and labels for p4d/p5 instances)",
130)
131@click.option("--output-file", "-o", help="Output manifest to file instead of applying")
132@pass_config
133def create_capacity_block_nodepool(
134 config: Any,
135 name: Any,
136 region: Any,
137 capacity_reservation_id: Any,
138 instance_type: Any,
139 max_nodes: Any,
140 fallback_on_demand: Any,
141 efa: Any,
142 output_file: Any,
143) -> None:
144 """Create a NodePool backed by a purchased Capacity Block.
146 Purchasing a Capacity Block ('gco capacity reserve') yields a normal EC2
147 Capacity Reservation ID (cr-xxx). This generates a Karpenter NodePool and
148 EC2NodeClass that consume it via capacityReservationSelectorTerms. Because a
149 Capacity Block is prepaid for a fixed term, the NodePool defaults to holding
150 that capacity rather than consolidating it away.
152 Examples:
153 gco nodepools create-capacity-block -n cb-train -r us-east-1 \\
154 -c cr-0123456789abcdef0 -i p5.48xlarge
156 gco nodepools create-capacity-block -n cb-efa -r us-east-1 \\
157 -c cr-0123456789abcdef0 -i p4d.24xlarge --efa
159 See: https://karpenter.sh/docs/concepts/nodeclasses/
160 """
161 from ..nodepools import generate_capacity_block_nodepool_manifest
163 formatter = get_output_formatter(config)
165 try:
166 manifest = generate_capacity_block_nodepool_manifest(
167 name=name,
168 region=region,
169 capacity_reservation_id=capacity_reservation_id,
170 instance_types=list(instance_type) if instance_type else None,
171 max_nodes=max_nodes,
172 fallback_on_demand=fallback_on_demand,
173 efa=efa,
174 project_name=config.project_name,
175 )
177 if output_file:
178 with open(output_file, "w", encoding="utf-8") as f:
179 f.write(manifest)
180 formatter.print_success(f"NodePool manifest written to {output_file}")
181 formatter.print_info(f"Apply with: kubectl apply -f {output_file}")
182 else:
183 print(manifest)
184 formatter.print_info("\nTo apply this manifest, save it to a file and run:")
185 formatter.print_info(" kubectl apply -f <filename>.yaml")
187 except Exception as e:
188 formatter.print_error(f"Failed to create Capacity Block NodePool: {e}")
189 sys.exit(1)
192@nodepools.command("delete")
193@click.argument("nodepool_name")
194@click.option("--region", "-r", required=True, help="AWS region")
195@click.option("--cluster", help="EKS cluster name (defaults to <project_name>-<region>)")
196@click.option("--yes", "-y", is_flag=True, help="Skip confirmation")
197@pass_config
198def delete_nodepool(config: Any, nodepool_name: Any, region: Any, cluster: Any, yes: Any) -> None:
199 """Delete a NodePool and its paired EC2NodeClass.
201 Removes the Karpenter NodePool and the ``<name>-nodeclass`` EC2NodeClass
202 created with it. Karpenter drains and terminates any nodes the NodePool
203 provisioned. Cannot be undone.
205 Examples:
206 gco nodepools delete gpu-reserved -r us-east-1
207 gco nodepools delete gpu-reserved -r us-east-1 -y
208 """
209 from ..nodepools import delete_cluster_nodepool
211 formatter = get_output_formatter(config)
213 if not yes:
214 click.confirm(f"Delete NodePool '{nodepool_name}' in {region}?", abort=True)
216 try:
217 cluster_name = cluster or f"{config.project_name}-{region}"
218 deleted = delete_cluster_nodepool(cluster_name, region, nodepool_name)
219 formatter.print_success(f"Deleted NodePool {nodepool_name}")
220 if deleted.get("ec2nodeclass"):
221 formatter.print_info(f"Deleted EC2NodeClass {deleted['ec2nodeclass']}")
222 except Exception as e:
223 formatter.print_error(f"Failed to delete NodePool: {e}")
224 sys.exit(1)
227@nodepools.command("list")
228@click.option("--region", "-r", help="Filter by region")
229@click.option("--cluster", help="EKS cluster name (defaults to <project_name>-<region>)")
230@pass_config
231def list_nodepools(config: Any, region: Any, cluster: Any) -> None:
232 """List NodePools in the cluster.
234 Examples:
235 gco nodepools list --region us-east-1
236 gco nodepools list --cluster my-cluster
237 """
238 from ..nodepools import list_cluster_nodepools
240 formatter = get_output_formatter(config)
242 try:
243 if not region and not cluster:
244 formatter.print_error("Either --region or --cluster is required")
245 sys.exit(1)
247 cluster_name = cluster or f"{config.project_name}-{region}"
248 nodepools_list = list_cluster_nodepools(cluster_name, region or config.default_region)
250 if not nodepools_list:
251 formatter.print_info("No NodePools found")
252 return
254 formatter.print(nodepools_list)
256 except Exception as e:
257 formatter.print_error(f"Failed to list NodePools: {e}")
258 sys.exit(1)
261@nodepools.command("describe")
262@click.argument("nodepool_name")
263@click.option("--region", "-r", required=True, help="AWS region")
264@click.option("--cluster", help="EKS cluster name (defaults to <project_name>-<region>)")
265@pass_config
266def describe_nodepool(config: Any, nodepool_name: Any, region: Any, cluster: Any) -> None:
267 """Describe a NodePool.
269 Examples:
270 gco nodepools describe gpu-x86-pool --region us-east-1
271 """
272 from ..nodepools import describe_cluster_nodepool
274 formatter = get_output_formatter(config)
276 try:
277 cluster_name = cluster or f"{config.project_name}-{region}"
278 nodepool = describe_cluster_nodepool(cluster_name, region, nodepool_name)
280 if not nodepool:
281 formatter.print_error(f"NodePool {nodepool_name} not found")
282 sys.exit(1)
284 formatter.print(nodepool)
286 except Exception as e:
287 formatter.print_error(f"Failed to describe NodePool: {e}")
288 sys.exit(1)