Skip to content

Commit de9bb5f

Browse files
authored
Merge pull request #91 from hieumoscow/main
feat: enhance AKS-MCP with Kubernetes placement operations
2 parents 5e66bcd + 7f61ffb commit de9bb5f

File tree

10 files changed

+1211
-24
lines changed

10 files changed

+1211
-24
lines changed

README.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ It allows AI tools to:
66

77
- Operate (CRUD) AKS resources
88
- Retrieve details related to AKS clusters (VNets, Subnets, NSGs, Route Tables, etc.)
9+
- Manage Azure Fleet operations for multi-cluster scenarios
910

1011
## How it works
1112

@@ -151,6 +152,14 @@ List all my AKS clusters in my subscription xxx.
151152
What is the network configuration of my AKS cluster?
152153

153154
Show me the network security groups associated with my cluster.
155+
156+
Create a new Azure Fleet named prod-fleet in eastus region.
157+
158+
List all members in my fleet.
159+
160+
Create a placement to deploy nginx workloads to clusters with app=frontend label.
161+
162+
Show me all ClusterResourcePlacements in my fleet.
154163
```
155164
156165
## Available Tools
@@ -231,9 +240,14 @@ The AKS-MCP server provides the following tools for interacting with AKS cluster
231240
<summary>Fleet Tools</summary>
232241
233242
- `az_fleet`: Execute Azure Fleet commands with structured parameters for AKS Fleet management
234-
- Supports operations: list, show, create, update, delete, start, stop
235-
- Supports resources: fleet, member, updaterun, updatestrategy
243+
- Supports operations: list, show, create, update, delete, start, stop, get-credentials
244+
- Supports resources: fleet, member, updaterun, updatestrategy, clusterresourceplacement
236245
- Requires readwrite or admin access for write operations
246+
- **Kubernetes ClusterResourcePlacement Operations**: Create and manage ClusterResourcePlacements
247+
- `clusterresourceplacement create`: Create new ClusterResourcePlacement with policy and selectors
248+
- `clusterresourceplacement list`: List all ClusterResourcePlacements
249+
- `clusterresourceplacement show/get`: Show ClusterResourcePlacement details
250+
- `clusterresourceplacement delete`: Delete ClusterResourcePlacement
237251
</details>
238252
239253
<details>

internal/azcli/fleet_executor.go

Lines changed: 205 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,23 @@ import (
44
"fmt"
55
"strings"
66

7+
"github.com/Azure/aks-mcp/internal/components/fleet/kubernetes"
78
"github.com/Azure/aks-mcp/internal/config"
89
)
910

1011
// FleetExecutor handles structured fleet command execution
1112
type FleetExecutor struct {
1213
*AzExecutor
14+
k8sClient *kubernetes.Client
15+
placementOps *kubernetes.PlacementOperations
16+
k8sClientInitialized bool
1317
}
1418

1519
// NewFleetExecutor creates a new fleet command executor
1620
func NewFleetExecutor() *FleetExecutor {
1721
return &FleetExecutor{
18-
AzExecutor: NewExecutor(),
22+
AzExecutor: NewExecutor(),
23+
k8sClientInitialized: false,
1924
}
2025
}
2126

@@ -37,16 +42,30 @@ func (e *FleetExecutor) Execute(params map[string]interface{}, cfg *config.Confi
3742
return "", fmt.Errorf("args parameter is required and must be a string")
3843
}
3944

40-
// Validate operation/resource combination
45+
// Route clusterresourceplacement operations to Kubernetes
46+
if resource == "clusterresourceplacement" {
47+
// Validate clusterresourceplacement operations separately
48+
if err := e.validateClusterResourcePlacementCombination(operation); err != nil {
49+
return "", err
50+
}
51+
return e.executeKubernetesClusterResourcePlacement(operation, args, cfg)
52+
}
53+
54+
// Validate operation/resource combination for non-placement resources
4155
if err := e.validateCombination(operation, resource); err != nil {
4256
return "", err
4357
}
4458

4559
// Construct the full command
46-
command := fmt.Sprintf("az fleet %s %s", resource, operation)
60+
var command string
4761
if operation == "list" && resource == "fleet" {
4862
// Special case: "az fleet list" without resource in between
4963
command = "az fleet list"
64+
} else if operation == "get-credentials" && resource == "fleet" {
65+
// Special case: "az fleet get-credentials"
66+
command = "az fleet get-credentials"
67+
} else {
68+
command = fmt.Sprintf("az fleet %s %s", resource, operation)
5069
}
5170

5271
// Check access level
@@ -72,7 +91,7 @@ func (e *FleetExecutor) Execute(params map[string]interface{}, cfg *config.Confi
7291
// validateCombination validates if the operation/resource combination is valid
7392
func (e *FleetExecutor) validateCombination(operation, resource string) error {
7493
validCombinations := map[string][]string{
75-
"fleet": {"list", "show", "create", "update", "delete"},
94+
"fleet": {"list", "show", "create", "update", "delete", "get-credentials"},
7695
"member": {"list", "show", "create", "update", "delete"},
7796
"updaterun": {"list", "show", "create", "start", "stop", "delete"},
7897
"updatestrategy": {"list", "show", "create", "delete"},
@@ -96,7 +115,7 @@ func (e *FleetExecutor) validateCombination(operation, resource string) error {
96115
// checkAccessLevel ensures the operation is allowed for the current access level
97116
func (e *FleetExecutor) checkAccessLevel(operation, resource string, accessLevel string) error {
98117
// Read-only operations are allowed for all access levels
99-
readOnlyOps := []string{"list", "show"}
118+
readOnlyOps := []string{"list", "show", "get", "get-credentials"}
100119
for _, op := range readOnlyOps {
101120
if operation == op {
102121
return nil
@@ -114,9 +133,13 @@ func (e *FleetExecutor) checkAccessLevel(operation, resource string, accessLevel
114133

115134
// GetCommandForValidation returns the constructed command for security validation
116135
func (e *FleetExecutor) GetCommandForValidation(operation, resource, args string) string {
117-
command := fmt.Sprintf("az fleet %s %s", resource, operation)
136+
var command string
118137
if operation == "list" && resource == "fleet" {
119138
command = "az fleet list"
139+
} else if operation == "get-credentials" && resource == "fleet" {
140+
command = "az fleet get-credentials"
141+
} else {
142+
command = fmt.Sprintf("az fleet %s %s", resource, operation)
120143
}
121144

122145
if args != "" {
@@ -125,3 +148,179 @@ func (e *FleetExecutor) GetCommandForValidation(operation, resource, args string
125148

126149
return command
127150
}
151+
152+
// executeKubernetesClusterResourcePlacement handles clusterresourceplacement operations via Kubernetes API
153+
func (e *FleetExecutor) executeKubernetesClusterResourcePlacement(operation, args string, cfg *config.ConfigData) (string, error) {
154+
// Check access level for clusterresourceplacement operations
155+
if err := e.checkAccessLevel(operation, "clusterresourceplacement", cfg.AccessLevel); err != nil {
156+
return "", err
157+
}
158+
159+
// Initialize Kubernetes client if needed
160+
if !e.k8sClientInitialized {
161+
if err := e.initializeKubernetesClient(); err != nil {
162+
return "", err
163+
}
164+
}
165+
166+
// Check if placement operations are initialized
167+
if e.placementOps == nil {
168+
return "", fmt.Errorf("clusterresourceplacement operations not initialized")
169+
}
170+
171+
// Parse arguments
172+
parsedArgs, parseErr := kubernetes.ParsePlacementArgs(args)
173+
if parseErr != nil {
174+
return "", fmt.Errorf("failed to parse clusterresourceplacement arguments: %w", parseErr)
175+
}
176+
177+
// Execute the clusterresourceplacement operation with error recovery
178+
var result string
179+
var err error
180+
181+
func() {
182+
defer func() {
183+
if r := recover(); r != nil {
184+
// Provide a helpful error message without stdout pollution
185+
err = fmt.Errorf("kubectl operation failed. Please ensure kubectl is installed, properly configured, and the cluster is accessible. Error: %v", r)
186+
}
187+
}()
188+
189+
switch operation {
190+
case "create":
191+
result, err = e.createClusterResourcePlacement(parsedArgs, cfg)
192+
case "get", "show":
193+
result, err = e.getClusterResourcePlacement(parsedArgs, cfg)
194+
case "list":
195+
result, err = e.placementOps.ListPlacements(cfg)
196+
case "delete":
197+
result, err = e.deleteClusterResourcePlacement(parsedArgs, cfg)
198+
default:
199+
err = fmt.Errorf("unsupported clusterresourceplacement operation: %s", operation)
200+
}
201+
202+
}()
203+
204+
// Clean and validate the result for MCP compatibility
205+
if err == nil {
206+
result = cleanResult(result)
207+
}
208+
209+
return result, err
210+
}
211+
212+
// cleanResult sanitizes the result for MCP compatibility
213+
func cleanResult(result string) string {
214+
// Just clean problematic characters and return as-is
215+
cleaned := result
216+
cleaned = strings.ReplaceAll(cleaned, "\x00", "") // Null bytes
217+
cleaned = strings.ReplaceAll(cleaned, "\r", "") // Carriage returns
218+
cleaned = strings.ReplaceAll(cleaned, "\x1b", "") // Escape sequences
219+
cleaned = strings.TrimSpace(cleaned)
220+
221+
return cleaned
222+
}
223+
224+
// initializeKubernetesClient initializes the Kubernetes client
225+
func (e *FleetExecutor) initializeKubernetesClient() error {
226+
defer func() {
227+
// Recover from any panics during client initialization
228+
if r := recover(); r != nil {
229+
e.k8sClientInitialized = false
230+
}
231+
}()
232+
233+
client, err := kubernetes.NewClient()
234+
if err != nil {
235+
return fmt.Errorf("failed to initialize Kubernetes client. Please ensure kubectl is installed and kubeconfig is properly configured: %w", err)
236+
}
237+
238+
// Test if the client is actually usable
239+
if client == nil {
240+
return fmt.Errorf("kubernetes client is nil after initialization")
241+
}
242+
243+
e.k8sClient = client
244+
e.placementOps = kubernetes.NewPlacementOperations(client)
245+
e.k8sClientInitialized = true
246+
247+
return nil
248+
}
249+
250+
// validateClusterResourcePlacementCombination validates clusterresourceplacement operations
251+
func (e *FleetExecutor) validateClusterResourcePlacementCombination(operation string) error {
252+
validOps := []string{"list", "show", "get", "create", "delete"}
253+
254+
for _, validOp := range validOps {
255+
if operation == validOp {
256+
return nil
257+
}
258+
}
259+
260+
return fmt.Errorf("invalid operation '%s' for resource 'clusterresourceplacement'. Valid operations: %s",
261+
operation, strings.Join(validOps, ", "))
262+
}
263+
264+
// createClusterResourcePlacement creates a clusterresourceplacement using placement operations
265+
func (e *FleetExecutor) createClusterResourcePlacement(args map[string]string, cfg *config.ConfigData) (string, error) {
266+
name, ok := args["name"]
267+
if !ok || name == "" {
268+
return "", fmt.Errorf("--name is required for create operation")
269+
}
270+
271+
selector := args["selector"]
272+
policy := args["policy"]
273+
274+
// Default policy if not specified
275+
if policy == "" {
276+
policy = "PickAll"
277+
}
278+
279+
// Validate policy
280+
validPolicies := []string{"PickAll", "PickFixed", "PickN"}
281+
isValidPolicy := false
282+
for _, validPolicy := range validPolicies {
283+
if strings.EqualFold(policy, validPolicy) {
284+
policy = validPolicy
285+
isValidPolicy = true
286+
break
287+
}
288+
}
289+
if !isValidPolicy {
290+
return "", fmt.Errorf("invalid policy '%s'. Valid policies: %s", policy, strings.Join(validPolicies, ", "))
291+
}
292+
293+
if e.placementOps == nil {
294+
return "", fmt.Errorf("clusterresourceplacement operations not initialized")
295+
}
296+
297+
return e.placementOps.CreatePlacement(name, selector, policy, cfg)
298+
}
299+
300+
// getClusterResourcePlacement retrieves a clusterresourceplacement using placement operations
301+
func (e *FleetExecutor) getClusterResourcePlacement(args map[string]string, cfg *config.ConfigData) (string, error) {
302+
name, ok := args["name"]
303+
if !ok || name == "" {
304+
return "", fmt.Errorf("--name is required for get/show operation")
305+
}
306+
307+
if e.placementOps == nil {
308+
return "", fmt.Errorf("clusterresourceplacement operations not initialized")
309+
}
310+
311+
return e.placementOps.GetPlacement(name, cfg)
312+
}
313+
314+
// deleteClusterResourcePlacement deletes a clusterresourceplacement using placement operations
315+
func (e *FleetExecutor) deleteClusterResourcePlacement(args map[string]string, cfg *config.ConfigData) (string, error) {
316+
name, ok := args["name"]
317+
if !ok || name == "" {
318+
return "", fmt.Errorf("--name is required for delete operation")
319+
}
320+
321+
if e.placementOps == nil {
322+
return "", fmt.Errorf("clusterresourceplacement operations not initialized")
323+
}
324+
325+
return e.placementOps.DeletePlacement(name, cfg)
326+
}

0 commit comments

Comments
 (0)