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

1"""Karpenter NodePool management MCP tools (read-only).""" 

2 

3import asyncio 

4 

5import cli_runner 

6from audit import audit_logged 

7from server import mcp 

8 

9 

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. 

14 

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) 

25 

26 

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. 

31 

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) 

41 

42 

43# ============================================================================= 

44# Mutating tools (low-risk) 

45# ============================================================================= 

46 

47 

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. 

60 

61 Generates a Karpenter NodePool + EC2NodeClass that consume an On-Demand 

62 Capacity Reservation via ``capacityReservationSelectorTerms``. 

63 

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) 

92 

93 

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. 

106 

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. 

112 

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) 

141 

142 

143# ============================================================================= 

144# Destructive tools — gated by GCO_ENABLE_DESTRUCTIVE_OPERATIONS 

145# ============================================================================= 

146 

147 

148import contextlib # noqa: E402 

149 

150from feature_flags import FLAG_DESTRUCTIVE_OPERATIONS, is_enabled # noqa: E402 

151 

152 

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 

157 

158 ctx = get_context() 

159 except Exception: 

160 return 

161 with contextlib.suppress(Exception): 

162 await ctx.warning(message) 

163 

164 

165if is_enabled(FLAG_DESTRUCTIVE_OPERATIONS): 

166 

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. 

171 

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. 

175 

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)