diff --git a/README.md b/README.md index 4be55fe..fbaf68e 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,15 @@ The AKS-MCP server provides the following tools for interacting with AKS cluster - `get_nsg_info`: Get information about the network security groups used by the AKS cluster +
+Fleet Tools + +- `az_fleet`: Execute Azure Fleet commands with structured parameters for AKS Fleet management + - Supports operations: list, show, create, update, delete, start, stop + - Supports resources: fleet, member, updaterun, updatestrategy + - Requires readwrite or admin access for write operations +
+ ## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a diff --git a/internal/azcli/fleet_executor.go b/internal/azcli/fleet_executor.go new file mode 100644 index 0000000..2d54fb4 --- /dev/null +++ b/internal/azcli/fleet_executor.go @@ -0,0 +1,127 @@ +package azcli + +import ( + "fmt" + "strings" + + "github.com/Azure/aks-mcp/internal/config" +) + +// FleetExecutor handles structured fleet command execution +type FleetExecutor struct { + *AzExecutor +} + +// NewFleetExecutor creates a new fleet command executor +func NewFleetExecutor() *FleetExecutor { + return &FleetExecutor{ + AzExecutor: NewExecutor(), + } +} + +// Execute processes structured fleet commands +func (e *FleetExecutor) Execute(params map[string]interface{}, cfg *config.ConfigData) (string, error) { + // Extract structured parameters + operation, ok := params["operation"].(string) + if !ok { + return "", fmt.Errorf("operation parameter is required and must be a string") + } + + resource, ok := params["resource"].(string) + if !ok { + return "", fmt.Errorf("resource parameter is required and must be a string") + } + + args, ok := params["args"].(string) + if !ok { + return "", fmt.Errorf("args parameter is required and must be a string") + } + + // Validate operation/resource combination + if err := e.validateCombination(operation, resource); err != nil { + return "", err + } + + // Construct the full command + command := fmt.Sprintf("az fleet %s %s", resource, operation) + if operation == "list" && resource == "fleet" { + // Special case: "az fleet list" without resource in between + command = "az fleet list" + } + + // Check access level + if err := e.checkAccessLevel(operation, resource, cfg.AccessLevel); err != nil { + return "", err + } + + // Build full command with args + fullCommand := command + if args != "" { + fullCommand = fmt.Sprintf("%s %s", command, args) + } + + // Create params for the base executor + execParams := map[string]interface{}{ + "command": fullCommand, + } + + // Execute using the base executor + return e.AzExecutor.Execute(execParams, cfg) +} + +// validateCombination validates if the operation/resource combination is valid +func (e *FleetExecutor) validateCombination(operation, resource string) error { + validCombinations := map[string][]string{ + "fleet": {"list", "show", "create", "update", "delete"}, + "member": {"list", "show", "create", "update", "delete"}, + "updaterun": {"list", "show", "create", "start", "stop", "delete"}, + "updatestrategy": {"list", "show", "create", "delete"}, + } + + validOps, exists := validCombinations[resource] + if !exists { + return fmt.Errorf("invalid resource type: %s", resource) + } + + for _, validOp := range validOps { + if operation == validOp { + return nil + } + } + + return fmt.Errorf("invalid operation '%s' for resource '%s'. Valid operations: %s", + operation, resource, strings.Join(validOps, ", ")) +} + +// checkAccessLevel ensures the operation is allowed for the current access level +func (e *FleetExecutor) checkAccessLevel(operation, resource string, accessLevel string) error { + // Read-only operations are allowed for all access levels + readOnlyOps := []string{"list", "show"} + for _, op := range readOnlyOps { + if operation == op { + return nil + } + } + + // Write operations require readwrite or admin access + if accessLevel == "readonly" { + return fmt.Errorf("operation '%s' requires readwrite or admin access level, current level is readonly", operation) + } + + // All operations are allowed for readwrite and admin + return nil +} + +// GetCommandForValidation returns the constructed command for security validation +func (e *FleetExecutor) GetCommandForValidation(operation, resource, args string) string { + command := fmt.Sprintf("az fleet %s %s", resource, operation) + if operation == "list" && resource == "fleet" { + command = "az fleet list" + } + + if args != "" { + command = fmt.Sprintf("%s %s", command, args) + } + + return command +} \ No newline at end of file diff --git a/internal/azcli/fleet_executor_test.go b/internal/azcli/fleet_executor_test.go new file mode 100644 index 0000000..11a3564 --- /dev/null +++ b/internal/azcli/fleet_executor_test.go @@ -0,0 +1,265 @@ +package azcli + +import ( + "strings" + "testing" + + "github.com/Azure/aks-mcp/internal/config" + "github.com/Azure/aks-mcp/internal/security" +) + +func TestFleetExecutor_ValidateCombination(t *testing.T) { + executor := NewFleetExecutor() + + tests := []struct { + name string + operation string + resource string + wantErr bool + errMsg string + }{ + { + name: "valid fleet list", + operation: "list", + resource: "fleet", + wantErr: false, + }, + { + name: "valid member create", + operation: "create", + resource: "member", + wantErr: false, + }, + { + name: "valid updaterun start", + operation: "start", + resource: "updaterun", + wantErr: false, + }, + { + name: "invalid operation for fleet", + operation: "start", + resource: "fleet", + wantErr: true, + errMsg: "invalid operation 'start' for resource 'fleet'", + }, + { + name: "invalid operation for updatestrategy", + operation: "update", + resource: "updatestrategy", + wantErr: true, + errMsg: "invalid operation 'update' for resource 'updatestrategy'", + }, + { + name: "invalid resource", + operation: "list", + resource: "invalid", + wantErr: true, + errMsg: "invalid resource type: invalid", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := executor.validateCombination(tt.operation, tt.resource) + if tt.wantErr { + if err == nil { + t.Errorf("validateCombination() error = nil, wantErr %v", tt.wantErr) + } else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) { + t.Errorf("validateCombination() error = %v, want error containing %v", err, tt.errMsg) + } + } else if err != nil { + t.Errorf("validateCombination() unexpected error = %v", err) + } + }) + } +} + +func TestFleetExecutor_CheckAccessLevel(t *testing.T) { + executor := NewFleetExecutor() + + tests := []struct { + name string + operation string + resource string + accessLevel string + wantErr bool + errMsg string + }{ + { + name: "readonly can list", + operation: "list", + resource: "fleet", + accessLevel: "readonly", + wantErr: false, + }, + { + name: "readonly can show", + operation: "show", + resource: "member", + accessLevel: "readonly", + wantErr: false, + }, + { + name: "readonly cannot create", + operation: "create", + resource: "fleet", + accessLevel: "readonly", + wantErr: true, + errMsg: "requires readwrite or admin access level", + }, + { + name: "readwrite can create", + operation: "create", + resource: "fleet", + accessLevel: "readwrite", + wantErr: false, + }, + { + name: "admin can delete", + operation: "delete", + resource: "updaterun", + accessLevel: "admin", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := executor.checkAccessLevel(tt.operation, tt.resource, tt.accessLevel) + if tt.wantErr { + if err == nil { + t.Errorf("checkAccessLevel() error = nil, wantErr %v", tt.wantErr) + } else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) { + t.Errorf("checkAccessLevel() error = %v, want error containing %v", err, tt.errMsg) + } + } else if err != nil { + t.Errorf("checkAccessLevel() unexpected error = %v", err) + } + }) + } +} + +func TestFleetExecutor_GetCommandForValidation(t *testing.T) { + executor := NewFleetExecutor() + + tests := []struct { + name string + operation string + resource string + args string + want string + }{ + { + name: "fleet list special case", + operation: "list", + resource: "fleet", + args: "--resource-group myRG", + want: "az fleet list --resource-group myRG", + }, + { + name: "member show with args", + operation: "show", + resource: "member", + args: "--name myMember --fleet-name myFleet --resource-group myRG", + want: "az fleet member show --name myMember --fleet-name myFleet --resource-group myRG", + }, + { + name: "updaterun create without args", + operation: "create", + resource: "updaterun", + args: "", + want: "az fleet updaterun create", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := executor.GetCommandForValidation(tt.operation, tt.resource, tt.args) + if got != tt.want { + t.Errorf("GetCommandForValidation() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestFleetExecutor_Execute(t *testing.T) { + // Note: This test validates parameter extraction and command construction + // but doesn't execute actual az commands + + tests := []struct { + name string + params map[string]interface{} + wantErr bool + errMsg string + }{ + { + name: "valid parameters", + params: map[string]interface{}{ + "operation": "list", + "resource": "fleet", + "args": "--resource-group myRG", + }, + wantErr: false, + }, + { + name: "missing operation", + params: map[string]interface{}{ + "resource": "fleet", + "args": "--resource-group myRG", + }, + wantErr: true, + errMsg: "operation parameter is required", + }, + { + name: "missing resource", + params: map[string]interface{}{ + "operation": "list", + "args": "--resource-group myRG", + }, + wantErr: true, + errMsg: "resource parameter is required", + }, + { + name: "missing args", + params: map[string]interface{}{ + "operation": "list", + "resource": "fleet", + }, + wantErr: true, + errMsg: "args parameter is required", + }, + { + name: "invalid combination", + params: map[string]interface{}{ + "operation": "start", + "resource": "fleet", + "args": "", + }, + wantErr: true, + errMsg: "invalid operation 'start' for resource 'fleet'", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + executor := NewFleetExecutor() + cfg := &config.ConfigData{ + AccessLevel: "readwrite", + SecurityConfig: &security.SecurityConfig{ + AccessLevel: "readwrite", + }, + } + + _, err := executor.Execute(tt.params, cfg) + if tt.wantErr { + if err == nil { + t.Errorf("Execute() error = nil, wantErr %v", tt.wantErr) + } else if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) { + t.Errorf("Execute() error = %v, want error containing %v", err, tt.errMsg) + } + } + // Note: We don't test successful execution as it would require mocking the az CLI + }) + } +} \ No newline at end of file diff --git a/internal/components/fleet/registry.go b/internal/components/fleet/registry.go index 0eb27cf..67436db 100644 --- a/internal/components/fleet/registry.go +++ b/internal/components/fleet/registry.go @@ -12,13 +12,34 @@ type FleetCommand struct { ArgsExample string // Example of command arguments } -// RegisterFleet registers the generic az fleet tool +// RegisterFleet registers the generic az fleet tool with structured parameters func RegisterFleet() mcp.Tool { + description := `Run Azure Kubernetes Service Fleet management commands. + +Available operations and resources: +- fleet: list, show, create, update, delete +- member: list, show, create, update, delete +- updaterun: list, show, create, start, stop, delete +- updatestrategy: list, show, create, delete + +Examples: +- List fleets: operation='list', resource='fleet', args='--resource-group myRG' +- Show fleet: operation='show', resource='fleet', args='--name myFleet --resource-group myRG' +- Create member: operation='create', resource='member', args='--name myMember --fleet-name myFleet --resource-group myRG --member-cluster-id /subscriptions/.../myCluster'` + return mcp.NewTool("az_fleet", - mcp.WithDescription("Run az fleet commands for Azure Kubernetes Service Fleet management"), - mcp.WithString("command", + mcp.WithDescription(description), + mcp.WithString("operation", + mcp.Required(), + mcp.Description("The operation to perform. Valid values: list, show, create, update, delete, start, stop"), + ), + mcp.WithString("resource", + mcp.Required(), + mcp.Description("The resource type to operate on. Valid values: fleet, member, updaterun, updatestrategy"), + ), + mcp.WithString("args", mcp.Required(), - mcp.Description("The az fleet command to execute (e.g., 'az fleet list', 'az fleet show --name myFleet --resource-group myRG')"), + mcp.Description("Additional arguments for the command (e.g., '--name myFleet --resource-group myRG')"), ), ) } diff --git a/internal/components/fleet/registry_test.go b/internal/components/fleet/registry_test.go index 9f75b17..76eb899 100644 --- a/internal/components/fleet/registry_test.go +++ b/internal/components/fleet/registry_test.go @@ -1,6 +1,7 @@ package fleet import ( + "strings" "testing" ) @@ -77,6 +78,37 @@ func TestRegisterFleetCommand_BasicCommands(t *testing.T) { } } +func TestRegisterFleet_StructuredParameters(t *testing.T) { + tool := RegisterFleet() + + // Test that the tool has the correct name + if tool.Name != "az_fleet" { + t.Errorf("Expected tool name 'az_fleet', got '%s'", tool.Name) + } + + // Test that the description contains important information + description := tool.Description + if !strings.Contains(description, "fleet: list, show, create, update, delete") { + t.Error("Expected description to contain fleet operations") + } + if !strings.Contains(description, "member: list, show, create, update, delete") { + t.Error("Expected description to contain member operations") + } + if !strings.Contains(description, "updaterun: list, show, create, start, stop, delete") { + t.Error("Expected description to contain updaterun operations") + } + if !strings.Contains(description, "Examples:") { + t.Error("Expected description to contain examples") + } + + // Test that the tool has the required parameters + // Note: We can't directly test the parameters without access to the tool's internal structure + // but we can verify that the tool was created successfully + if tool.Name == "" { + t.Error("Expected tool to have a name") + } +} + func TestGetReadWriteFleetCommands_ContainsManagementCommands(t *testing.T) { commands := GetReadWriteFleetCommands() diff --git a/internal/server/server.go b/internal/server/server.go index 17ba9d0..990011e 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -103,10 +103,10 @@ func (s *Service) registerAzCommands() { s.mcpServer.AddTool(azTool, tools.CreateToolHandler(commandExecutor, s.cfg)) } - // Register generic az fleet tool (available at all access levels) + // Register generic az fleet tool with structured parameters (available at all access levels) log.Println("Registering az fleet tool: az_fleet") fleetTool := fleet.RegisterFleet() - s.mcpServer.AddTool(fleetTool, tools.CreateToolHandler(azcli.NewExecutor(), s.cfg)) + s.mcpServer.AddTool(fleetTool, tools.CreateToolHandler(azcli.NewFleetExecutor(), s.cfg)) // Register Azure Resource Health monitoring tool (available at all access levels) log.Println("Registering monitor tool: az_monitor_activity_log_resource_health") diff --git a/prompts/azure-fleet-tools.md b/prompts/azure-fleet-tools.md index 1be3c98..5520478 100644 --- a/prompts/azure-fleet-tools.md +++ b/prompts/azure-fleet-tools.md @@ -11,16 +11,32 @@ This component provides Azure Fleet command-line tools for managing AKS Fleet re ### Generic Fleet Tool #### `az_fleet` -**Purpose**: Execute any Azure Fleet command for AKS Fleet management +**Purpose**: Execute Azure Fleet commands for AKS Fleet management with structured parameters **Parameters**: -- `command` (required): The complete az fleet command to execute +- `operation` (required): The operation to perform (list, show, create, update, delete, start, stop) +- `resource` (required): The resource type to operate on (fleet, member, updaterun, updatestrategy) +- `args` (required): Additional arguments for the command **Example Usage**: -```bash -az fleet list --resource-group myResourceGroup -az fleet show --name myFleet --resource-group myResourceGroup -az fleet create --name myFleet --resource-group myResourceGroup --location eastus +```json +{ + "operation": "list", + "resource": "fleet", + "args": "--resource-group myResourceGroup" +} + +{ + "operation": "show", + "resource": "fleet", + "args": "--name myFleet --resource-group myResourceGroup" +} + +{ + "operation": "create", + "resource": "fleet", + "args": "--name myFleet --resource-group myResourceGroup --location eastus" +} ``` ## Fleet Command Categories @@ -76,13 +92,17 @@ az fleet create --name myFleet --resource-group myResourceGroup --location eastu internal/components/fleet/ ├── registry.go # Fleet tool registration and command definitions └── registry_test.go # Unit tests for the registry + +internal/azcli/ +├── fleet_executor.go # Specialized fleet command executor with parameter validation +└── fleet_executor_test.go # Unit tests for the fleet executor ``` ### Tool Registration A single generic fleet tool is registered in the MCP server: -- **Generic Tool**: `az_fleet` - Accepts any fleet command through the "command" parameter +- **Generic Tool**: `az_fleet` - Accepts structured parameters: operation, resource, and args - **Access Control**: Commands are validated against the configured access level through security validation -- **Execution**: Uses the generic `azcli.NewExecutor()` for command execution +- **Execution**: Uses the specialized `azcli.NewFleetExecutor()` for command execution and parameter validation ### Fleet Command Structure Fleet commands are organized using the `FleetCommand` structure for documentation: @@ -97,10 +117,10 @@ type FleetCommand struct { ### Integration with Server The fleet tool is registered in `internal/server/server.go`: ```go -// Register generic az fleet tool (available at all access levels) +// Register generic az fleet tool with structured parameters (available at all access levels) log.Println("Registering az fleet tool: az_fleet") fleetTool := fleet.RegisterFleet() -s.mcpServer.AddTool(fleetTool, tools.CreateToolHandler(azcli.NewExecutor(), s.cfg)) +s.mcpServer.AddTool(fleetTool, tools.CreateToolHandler(azcli.NewFleetExecutor(), s.cfg)) ``` ## Access Level Requirements @@ -179,12 +199,14 @@ az fleet updatestrategy list --fleet-name myFleet --resource-group myResourceGro ## Error Handling -The fleet tools leverage the existing error handling infrastructure: +The fleet tools leverage enhanced error handling infrastructure: - Azure CLI authentication errors are handled gracefully - Invalid fleet or member names return descriptive error messages - Network connectivity issues are properly reported - Malformed arguments are validated before execution - Access level violations are caught by security validation +- **Parameter Validation**: Operation/resource combinations are validated (e.g., 'start' is only valid for 'updaterun') +- **Access Level Enforcement**: Read-only operations (list, show) are allowed at all access levels, while write operations require readwrite or admin access ## Security and Access Control @@ -201,12 +223,20 @@ Fleet commands are subject to the same security validation as other Azure CLI co "Please show me all fleets in my production resource group" This would translate to: -az_fleet with command: "az fleet list --resource-group production" +az_fleet with parameters: { + "operation": "list", + "resource": "fleet", + "args": "--resource-group production" +} "Add the cluster 'web-cluster' to my fleet 'prod-fleet'" This would translate to: -az_fleet with command: "az fleet member create --name web-cluster --fleet-name prod-fleet --resource-group production --member-cluster-id /subscriptions/.../managedClusters/web-cluster" +az_fleet with parameters: { + "operation": "create", + "resource": "member", + "args": "--name web-cluster --fleet-name prod-fleet --resource-group production --member-cluster-id /subscriptions/.../managedClusters/web-cluster" +} ``` ## Requirements @@ -260,6 +290,9 @@ Configure command execution timeout: - Cross-region fleet management - Integration with GitOps workflows - Fleet monitoring and alerting +- **Enhanced Parameter Validation**: More granular validation of command arguments +- **Smart Defaults**: Automatic parameter inference based on context +- **Batch Operations**: Support for bulk fleet operations ## Best Practices