diff --git a/internal/components/monitor/handlers.go b/internal/components/monitor/handlers.go new file mode 100644 index 0000000..8015081 --- /dev/null +++ b/internal/components/monitor/handlers.go @@ -0,0 +1,127 @@ +package monitor + +import ( + "fmt" + "strings" + "time" + + "github.com/Azure/aks-mcp/internal/azcli" + "github.com/Azure/aks-mcp/internal/config" + "github.com/Azure/aks-mcp/internal/tools" +) + +// HandleResourceHealthQuery handles the resource health query for AKS clusters +func HandleResourceHealthQuery(params map[string]interface{}, cfg *config.ConfigData) (string, error) { + // Extract and validate parameters + subscriptionID, ok := params["subscription_id"].(string) + if !ok || subscriptionID == "" { + return "", fmt.Errorf("missing or invalid subscription_id parameter") + } + + resourceGroup, ok := params["resource_group"].(string) + if !ok || resourceGroup == "" { + return "", fmt.Errorf("missing or invalid resource_group parameter") + } + + clusterName, ok := params["cluster_name"].(string) + if !ok || clusterName == "" { + return "", fmt.Errorf("missing or invalid cluster_name parameter") + } + + startTime, ok := params["start_time"].(string) + if !ok || startTime == "" { + return "", fmt.Errorf("missing or invalid start_time parameter") + } + + // Validate parameters + if err := validateResourceHealthParams(params); err != nil { + return "", err + } + + // Build resource ID + resourceID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.ContainerService/managedClusters/%s", + subscriptionID, resourceGroup, clusterName) + + // Build Azure CLI command + executor := azcli.NewExecutor() + args := []string{ + "monitor", "activity-log", "list", + "--resource-id", resourceID, + "--start-time", startTime, + "--query", "[?category.value=='ResourceHealth']", + "--output", "json", + } + + // Add end time if provided + if endTime, ok := params["end_time"].(string); ok && endTime != "" { + args = append(args, "--end-time", endTime) + } + + // Add status filter if provided + if status, ok := params["status"].(string); ok && status != "" { + // Apply status filter in the query + statusFilter := fmt.Sprintf("[?category.value=='ResourceHealth' && properties.currentHealthStatus=='%s']", status) + args[len(args)-3] = statusFilter // Replace the query parameter + } + + // Execute command + cmdParams := map[string]interface{}{ + "command": "az " + strings.Join(args, " "), + } + + result, err := executor.Execute(cmdParams, cfg) + if err != nil { + return "", fmt.Errorf("failed to execute resource health query: %w", err) + } + + // Return the raw JSON result from Azure CLI + return result, nil +} + +// validateResourceHealthParams validates the parameters for resource health queries +func validateResourceHealthParams(params map[string]interface{}) error { + // Validate required parameters + required := []string{"subscription_id", "resource_group", "cluster_name", "start_time"} + for _, param := range required { + if value, ok := params[param].(string); !ok || value == "" { + return fmt.Errorf("missing or invalid %s parameter", param) + } + } + + // Validate time format + startTime := params["start_time"].(string) + if _, err := time.Parse(time.RFC3339, startTime); err != nil { + return fmt.Errorf("invalid start_time format, expected RFC3339 (ISO 8601): %w", err) + } + + // Validate end_time if provided + if endTime, ok := params["end_time"].(string); ok && endTime != "" { + if _, err := time.Parse(time.RFC3339, endTime); err != nil { + return fmt.Errorf("invalid end_time format, expected RFC3339 (ISO 8601): %w", err) + } + } + + // Validate status if provided + if status, ok := params["status"].(string); ok && status != "" { + validStatuses := []string{"Available", "Unavailable", "Degraded", "Unknown"} + valid := false + for _, validStatus := range validStatuses { + if status == validStatus { + valid = true + break + } + } + if !valid { + return fmt.Errorf("invalid status parameter, must be one of: %s", strings.Join(validStatuses, ", ")) + } + } + + return nil +} + +// GetResourceHealthHandler returns a ResourceHandler for the resource health tool +func GetResourceHealthHandler(cfg *config.ConfigData) tools.ResourceHandler { + return tools.ResourceHandlerFunc(func(params map[string]interface{}, _ *config.ConfigData) (string, error) { + return HandleResourceHealthQuery(params, cfg) + }) +} diff --git a/internal/components/monitor/registry.go b/internal/components/monitor/registry.go index 5b2c68b..b2a40ef 100644 --- a/internal/components/monitor/registry.go +++ b/internal/components/monitor/registry.go @@ -69,3 +69,32 @@ func GetReadWriteMonitorCommands() []MonitorCommand { func GetAdminMonitorCommands() []MonitorCommand { return []MonitorCommand{} } + +// RegisterResourceHealthTool registers the Azure Resource Health monitoring tool +func RegisterResourceHealthTool() mcp.Tool { + return mcp.NewTool("az_monitor_activity_log_resource_health", + mcp.WithDescription("Retrieve resource health events for AKS clusters to monitor service availability and health status"), + mcp.WithString("subscription_id", + mcp.Required(), + mcp.Description("Azure subscription ID"), + ), + mcp.WithString("resource_group", + mcp.Required(), + mcp.Description("Resource group name containing the AKS cluster"), + ), + mcp.WithString("cluster_name", + mcp.Required(), + mcp.Description("AKS cluster name"), + ), + mcp.WithString("start_time", + mcp.Required(), + mcp.Description("Start date for health event query (ISO 8601 format, e.g., \"2025-01-01T00:00:00Z\")"), + ), + mcp.WithString("end_time", + mcp.Description("End date for health event query (defaults to current time)"), + ), + mcp.WithString("status", + mcp.Description("Filter by health status (Available, Unavailable, Degraded, Unknown)"), + ), + ) +} diff --git a/internal/security/validator.go b/internal/security/validator.go index 932bcf5..c6e02d4 100644 --- a/internal/security/validator.go +++ b/internal/security/validator.go @@ -56,6 +56,7 @@ var ( "az monitor metrics list", "az monitor metrics list-definitions", "az monitor metrics list-namespaces", + "az monitor activity-log list", // Other general commands "az find", diff --git a/internal/server/server.go b/internal/server/server.go index 74f2ab2..4f03e0b 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -101,6 +101,11 @@ func (s *Service) registerAzCommands() { s.mcpServer.AddTool(azTool, tools.CreateToolHandler(commandExecutor, s.cfg)) } + // Register Azure Resource Health monitoring tool (available at all access levels) + log.Println("Registering monitor tool: az_monitor_activity_log_resource_health") + resourceHealthTool := monitor.RegisterResourceHealthTool() + s.mcpServer.AddTool(resourceHealthTool, tools.CreateResourceHandler(monitor.GetResourceHealthHandler(s.cfg), s.cfg)) + // Register account management commands (available at all access levels) for _, cmd := range azaks.GetAccountAzCommands() { log.Println("Registering az command:", cmd.Name) diff --git a/prompts/azure-resource-health.md b/prompts/azure-resource-health.md index 4efc71a..e69de29 100644 Binary files a/prompts/azure-resource-health.md and b/prompts/azure-resource-health.md differ