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

1"""NodePool 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 nodepools(config: Any) -> None: 

17 """Manage Karpenter NodePools with ODCR/Capacity Reservation support.""" 

18 pass 

19 

20 

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

57 

58 This creates a Karpenter NodePool and EC2NodeClass configured to use 

59 a specific capacity reservation for guaranteed capacity. 

60 

61 Examples: 

62 gco nodepools create-odcr -n gpu-reserved -r us-east-1 \\ 

63 -c cr-0123456789abcdef0 -i p4d.24xlarge 

64 

65 gco nodepools create-odcr -n ml-training -r us-west-2 \\ 

66 -c cr-0123456789abcdef0 -i p5.48xlarge --fallback-on-demand 

67 

68 gco nodepools create-odcr -n efa-training -r us-east-1 \\ 

69 -c cr-0123456789abcdef0 -i p4d.24xlarge --efa 

70 

71 See: https://karpenter.sh/docs/tasks/odcrs/ 

72 """ 

73 from ..nodepools import generate_odcr_nodepool_manifest 

74 

75 formatter = get_output_formatter(config) 

76 

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 ) 

88 

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

99 

100 except Exception as e: 

101 formatter.print_error(f"Failed to create ODCR NodePool: {e}") 

102 sys.exit(1) 

103 

104 

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. 

145 

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. 

151 

152 Examples: 

153 gco nodepools create-capacity-block -n cb-train -r us-east-1 \\ 

154 -c cr-0123456789abcdef0 -i p5.48xlarge 

155 

156 gco nodepools create-capacity-block -n cb-efa -r us-east-1 \\ 

157 -c cr-0123456789abcdef0 -i p4d.24xlarge --efa 

158 

159 See: https://karpenter.sh/docs/concepts/nodeclasses/ 

160 """ 

161 from ..nodepools import generate_capacity_block_nodepool_manifest 

162 

163 formatter = get_output_formatter(config) 

164 

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 ) 

176 

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

186 

187 except Exception as e: 

188 formatter.print_error(f"Failed to create Capacity Block NodePool: {e}") 

189 sys.exit(1) 

190 

191 

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. 

200 

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. 

204 

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 

210 

211 formatter = get_output_formatter(config) 

212 

213 if not yes: 

214 click.confirm(f"Delete NodePool '{nodepool_name}' in {region}?", abort=True) 

215 

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) 

225 

226 

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. 

233 

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 

239 

240 formatter = get_output_formatter(config) 

241 

242 try: 

243 if not region and not cluster: 

244 formatter.print_error("Either --region or --cluster is required") 

245 sys.exit(1) 

246 

247 cluster_name = cluster or f"{config.project_name}-{region}" 

248 nodepools_list = list_cluster_nodepools(cluster_name, region or config.default_region) 

249 

250 if not nodepools_list: 

251 formatter.print_info("No NodePools found") 

252 return 

253 

254 formatter.print(nodepools_list) 

255 

256 except Exception as e: 

257 formatter.print_error(f"Failed to list NodePools: {e}") 

258 sys.exit(1) 

259 

260 

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. 

268 

269 Examples: 

270 gco nodepools describe gpu-x86-pool --region us-east-1 

271 """ 

272 from ..nodepools import describe_cluster_nodepool 

273 

274 formatter = get_output_formatter(config) 

275 

276 try: 

277 cluster_name = cluster or f"{config.project_name}-{region}" 

278 nodepool = describe_cluster_nodepool(cluster_name, region, nodepool_name) 

279 

280 if not nodepool: 

281 formatter.print_error(f"NodePool {nodepool_name} not found") 

282 sys.exit(1) 

283 

284 formatter.print(nodepool) 

285 

286 except Exception as e: 

287 formatter.print_error(f"Failed to describe NodePool: {e}") 

288 sys.exit(1)