Coverage for gco_mcp/tools/nodepools.py: 97.59%
61 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"""Karpenter NodePool management MCP tools (read-only)."""
3import asyncio
5import cli_runner
6from audit import audit_logged
7from server import mcp
10@mcp.tool(tags={"safe", "nodepools"})
11@audit_logged
12async def nodepools_list(region: str | None = None, cluster: str | None = None) -> str:
13 """`gco nodepools list` — list Karpenter NodePools in a cluster.
15 Args:
16 region: AWS region to query.
17 cluster: EKS cluster name (defaults to ``gco-<region>``).
18 """
19 args = ["nodepools", "list"]
20 if region:
21 args += ["-r", region]
22 if cluster:
23 args += ["--cluster", cluster]
24 return await asyncio.to_thread(cli_runner._run_cli, *args)
27@mcp.tool(tags={"safe", "nodepools"})
28@audit_logged
29async def nodepools_describe(nodepool_name: str, region: str, cluster: str | None = None) -> str:
30 """`gco nodepools describe` — describe a single NodePool.
32 Args:
33 nodepool_name: NodePool name.
34 region: AWS region.
35 cluster: EKS cluster name (defaults to ``gco-<region>``).
36 """
37 args = ["nodepools", "describe", nodepool_name, "-r", region]
38 if cluster:
39 args += ["--cluster", cluster]
40 return await asyncio.to_thread(cli_runner._run_cli, *args)
43# =============================================================================
44# Mutating tools (low-risk)
45# =============================================================================
48@mcp.tool(tags={"low-risk", "nodepools"})
49@audit_logged
50async def nodepools_create_odcr(
51 name: str,
52 region: str,
53 capacity_reservation_id: str,
54 instance_type: list[str] | None = None,
55 max_nodes: int = 100,
56 fallback_on_demand: bool = False,
57 efa: bool = False,
58) -> str:
59 """`gco nodepools create-odcr` — create a Karpenter NodePool tied to an ODCR.
61 Generates a Karpenter NodePool + EC2NodeClass that consume an On-Demand
62 Capacity Reservation via ``capacityReservationSelectorTerms``.
64 Args:
65 name: NodePool name.
66 region: AWS region.
67 capacity_reservation_id: EC2 Capacity Reservation ID (``cr-...``) or ODCR group ARN.
68 instance_type: Instance types the NodePool may provision (one per entry).
69 max_nodes: Maximum nodes in the pool.
70 fallback_on_demand: Fall back to on-demand when the ODCR is exhausted.
71 efa: Enable EFA support (adds EFA taint and labels).
72 """
73 args = [
74 "nodepools",
75 "create-odcr",
76 "-n",
77 name,
78 "-r",
79 region,
80 "-c",
81 capacity_reservation_id,
82 "--max-nodes",
83 str(max_nodes),
84 ]
85 for it in instance_type or []:
86 args += ["-i", it]
87 if fallback_on_demand:
88 args.append("--fallback-on-demand")
89 if efa:
90 args.append("--efa")
91 return await asyncio.to_thread(cli_runner._run_cli, *args)
94@mcp.tool(tags={"low-risk", "nodepools"})
95@audit_logged
96async def nodepools_create_capacity_block(
97 name: str,
98 region: str,
99 capacity_reservation_id: str,
100 instance_type: list[str] | None = None,
101 max_nodes: int = 100,
102 fallback_on_demand: bool = False,
103 efa: bool = False,
104) -> str:
105 """`gco nodepools create-capacity-block` — NodePool for a purchased Capacity Block.
107 The Capacity Block counterpart to ``nodepools_create_odcr``. Purchasing a
108 Capacity Block yields an EC2 Capacity Reservation id (``cr-...``), which this
109 NodePool consumes via ``capacityReservationSelectorTerms``. Because a block is
110 prepaid for a fixed term, the NodePool holds the capacity rather than
111 consolidating it early.
113 Args:
114 name: NodePool name.
115 region: AWS region.
116 capacity_reservation_id: Capacity Reservation ID (``cr-...``) of the block.
117 instance_type: Instance types the NodePool may provision (one per entry).
118 max_nodes: Maximum nodes in the pool.
119 fallback_on_demand: Fall back to on-demand when the block is exhausted/expired.
120 efa: Enable EFA support (adds EFA taint and labels).
121 """
122 args = [
123 "nodepools",
124 "create-capacity-block",
125 "-n",
126 name,
127 "-r",
128 region,
129 "-c",
130 capacity_reservation_id,
131 "--max-nodes",
132 str(max_nodes),
133 ]
134 for it in instance_type or []:
135 args += ["-i", it]
136 if fallback_on_demand:
137 args.append("--fallback-on-demand")
138 if efa:
139 args.append("--efa")
140 return await asyncio.to_thread(cli_runner._run_cli, *args)
143# =============================================================================
144# Destructive tools — gated by GCO_ENABLE_DESTRUCTIVE_OPERATIONS
145# =============================================================================
148import contextlib # noqa: E402
150from feature_flags import FLAG_DESTRUCTIVE_OPERATIONS, is_enabled # noqa: E402
153async def _ctx_warning(message: str) -> None:
154 """Emit ``ctx.warning(...)`` from inside a tool body, no-op when no Context."""
155 try:
156 from fastmcp.server.dependencies import get_context
158 ctx = get_context()
159 except Exception:
160 return
161 with contextlib.suppress(Exception):
162 await ctx.warning(message)
165if is_enabled(FLAG_DESTRUCTIVE_OPERATIONS):
167 @mcp.tool(tags={"destructive", "nodepools"})
168 @audit_logged
169 async def delete_nodepool(nodepool_name: str, region: str, cluster: str | None = None) -> str:
170 """[gated by GCO_ENABLE_DESTRUCTIVE_OPERATIONS] destructive.
172 `gco nodepools delete` — delete a Karpenter NodePool.
173 Cannot be undone — the NodePool, its EC2NodeClass, and any nodes
174 currently provisioned through it are removed.
176 Args:
177 nodepool_name: NodePool name.
178 region: AWS region.
179 cluster: EKS cluster name (defaults to ``gco-<region>``).
180 """
181 await _ctx_warning(
182 f"Deleting NodePool {nodepool_name!r} in {region} — this cannot be undone."
183 )
184 args = ["nodepools", "delete", nodepool_name, "-r", region, "-y"]
185 if cluster:
186 args += ["--cluster", cluster]
187 return await asyncio.to_thread(cli_runner._run_cli, *args)