diff --git a/docs/openshift/NETEDGE.md b/docs/openshift/NETEDGE.md new file mode 100644 index 000000000..b99664fd6 --- /dev/null +++ b/docs/openshift/NETEDGE.md @@ -0,0 +1,149 @@ +# Network Ingress&DNS (netedge) Toolset + +This document provides guidance on using the Network Ingress&DNS (formerly known as Network Edge) toolset with the Kubernetes MCP Server. + +## Available Tools + +The netedge toolset provides tools for monitoring and troubleshooting OpenShift network ingress and DNS components. + +### Router Tools + +Tools to inspect and monitor OpenShift router pods (HAProxy): + +| Tool | Description | Parameters | +|------|-------------|------------| +| `get_router_config` | Retrieve the current router's HAProxy configuration | `pod` (optional) - Router pod name | +| `get_router_info` | Retrieve HAProxy runtime information from the router | `pod` (optional) - Router pod name | +| `get_router_sessions` | Retrieve all active sessions from the router | `pod` (optional) - Router pod name | + +### DNS Tools + +Tools to inspect and monitor CoreDNS configuration: + +| Tool | Description | Parameters | +|------|-------------|------------| +| `get_coredns_config` | Retrieve the current CoreDNS configuration (Corefile) | None | + +**Notes**: +- All tools have an optional `pod` parameter. If not provided, the tools will automatically select any available router pod from the default ingress controller. +- Currently, the default ingress controller name (`default`) is hard-coded. Support for custom ingress controller names may be added in future versions. + +## Examples + +### View Router Configuration + +View the router configuration file from any router pod: + +``` +Show me the router configuration + +● I'll get the router configuration for you. + ⎿  # Router configuration (pod: router-default-6fbcf6cb84-vqthh) + ``` + global + … +333 lines (ctrl+o to expand) + +● Here's the router configuration from pod router-default-6fbcf6cb84-vqthh. This is an HAProxy configuration for the OpenShift router with the following key settings: + + Global Settings: + - Max connections: 50,000 + - 4 worker threads + - TLS: Minimum version TLSv1.2 + - Cipher suites: Modern secure ciphers (ECDHE-ECDSA, ECDHE-RSA, etc.) + - DH parameter size: 2048 bits + + Timeout Settings: + - Connect: 5s + - Client/Server: 30s + - HTTP request: 10s + - HTTP keep-alive: 300s + - WebSocket tunnel: 1h + - Console: 5 minutes +``` + +Or specify a particular router pod: + +``` +Show me the router configuration from pod router-default-6fbcf6cb84-vqthh +``` + +### View Router Runtime Information + +Get router runtime statistics and information: + +``` +Show me the router info + ⎿  # Router HAProxy info (pod: router-default-6fbcf6cb84-vqthh) + ``` + Name: HAProxy + … +76 lines (ctrl+o to expand) + +● Here's the HAProxy runtime information for the router (pod: router-default-6fbcf6cb84-vqthh): + + Version Information: + - HAProxy Version: 2.8.10-f28885f + - Release Date: 2024/06/14 + - Uptime: 1h 45m 7s + + Process Information: + - PID: 138 + - Threads: 4 + - Processes: 1 + + Connection Statistics: + - Current Connections: 10 + - Max Connections: 50,000 + - Cumulative Connections: 2,253 + - Cumulative Requests: 4,668 + - Max Connection Rate: 12/sec + + SSL/TLS Statistics: + - Current SSL Connections: 1 + - Cumulative SSL Connections: 862 + - SSL Frontend Max Key Rate: 4/sec + - SSL Backend Max Key Rate: 1/sec +``` + +This displays: +- Router version and uptime +- Process information +- Connection statistics +- Request rates +- Memory usage + +### View Active Sessions + +View all active sessions in the router: + +``` +Show me active router sessions + ⎿  # Router active sessions (pod: router-default-6fbcf6cb84-vqthh) + ``` + 0x7f223405ae50: [12/Jan/2026:22:35:39.393119] id=4697 proto=unix_stream source=unix:1 + … +17 lines (ctrl+o to expand) + +● Here are the active sessions in the router (pod: router-default-6fbcf6cb84-vqthh): + + There is currently 1 active session: + + Session ID 4697 (CLI Connection) + - Protocol: Unix stream socket + - Frontend: GLOBAL (HAProxy admin socket) + - State: ESTABLISHED + - Age: 0s (just created) + - Type: CLI applet connection + + Details: + - Connection flags: 0x8 + - Task state: running with high priority (nice=-64) + - File descriptor: 23 + - Buffers: 32KB request/response buffers allocated + - Request data: 14 bytes total (the command itself) + - Timeouts: 2 minutes for client, no write timeout +``` + +This displays detailed information about each active connection including: +- Session ID and protocol +- Connection state +- Frontend and backend information +- Task and file descriptor details diff --git a/evals/tasks/netedge/get-router-config/task.yaml b/evals/tasks/netedge/get-router-config/task.yaml new file mode 100644 index 000000000..8ee5a323f --- /dev/null +++ b/evals/tasks/netedge/get-router-config/task.yaml @@ -0,0 +1,12 @@ +kind: Task +metadata: + name: get-router-config +steps: + verify: + contains: "frontend public" + prompt: + inline: Retrieve and display the current HAProxy configuration from the OpenShift router. + assertions: + toolsUsed: + - server: kubernetes + toolPattern: "netedge__get_router_config" diff --git a/evals/tasks/netedge/get-router-info/task.yaml b/evals/tasks/netedge/get-router-info/task.yaml new file mode 100644 index 000000000..cea5b927b --- /dev/null +++ b/evals/tasks/netedge/get-router-info/task.yaml @@ -0,0 +1,12 @@ +kind: Task +metadata: + name: get-router-info +steps: + verify: + contains: "HAProxy Version" + prompt: + inline: Retrieve and display HAProxy runtime information and statistics from the OpenShift router. + assertions: + toolsUsed: + - server: kubernetes + toolPattern: "netedge__get_router_info" diff --git a/evals/tasks/netedge/get-router-sessions/task.yaml b/evals/tasks/netedge/get-router-sessions/task.yaml new file mode 100644 index 000000000..6525b66ae --- /dev/null +++ b/evals/tasks/netedge/get-router-sessions/task.yaml @@ -0,0 +1,12 @@ +kind: Task +metadata: + name: get-router-sessions +steps: + verify: + contains: "frontend=GLOBAL" + prompt: + inline: Retrieve and display all active sessions from the OpenShift router. + assertions: + toolsUsed: + - server: kubernetes + toolPattern: "netedge__get_router_sessions" diff --git a/pkg/toolsets/netedge/router.go b/pkg/toolsets/netedge/router.go new file mode 100644 index 000000000..ca911b59c --- /dev/null +++ b/pkg/toolsets/netedge/router.go @@ -0,0 +1,206 @@ +package netedge + +import ( + "errors" + "fmt" + "strings" + + "github.com/google/jsonschema-go/jsonschema" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/utils/ptr" + + "github.com/containers/kubernetes-mcp-server/pkg/api" + "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" +) + +const ( + ingressNamespace = "openshift-ingress" + defaultIngressControllerName = "default" + routerContainerName = "router" +) + +func initRouter() []api.ServerTool { + return []api.ServerTool{ + { + Tool: api.Tool{ + Name: "get_router_config", + Description: `Retrieve the current router's HAProxy configuration from the cluster.`, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "pod": { + Type: "string", + Description: "Router pod name (optional, chooses any existing if not provided)", + }, + }, + }, + Annotations: api.ToolAnnotations{ + Title: "Get Router Config", + ReadOnlyHint: ptr.To(true), + DestructiveHint: ptr.To(false), + OpenWorldHint: ptr.To(true), + }, + }, + Handler: getRouterConfig, + }, + { + Tool: api.Tool{ + Name: "get_router_info", + Description: `Retrieve HAProxy runtime information from the router.`, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "pod": { + Type: "string", + Description: "Router pod name (optional, chooses any existing if not provided)", + }, + }, + }, + Annotations: api.ToolAnnotations{ + Title: "Get Router Info", + ReadOnlyHint: ptr.To(true), + DestructiveHint: ptr.To(false), + OpenWorldHint: ptr.To(true), + }, + }, + Handler: getRouterInfo, + }, + { + Tool: api.Tool{ + Name: "get_router_sessions", + Description: `Retrieve all active sessions from the router.`, + InputSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "pod": { + Type: "string", + Description: "Router pod name (optional, chooses any existing if not provided)", + }, + }, + }, + Annotations: api.ToolAnnotations{ + Title: "Get Router Sessions", + ReadOnlyHint: ptr.To(true), + DestructiveHint: ptr.To(false), + OpenWorldHint: ptr.To(true), + }, + }, + Handler: getRouterSessions, + }, + } +} + +func getRouterConfig(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + var results []string + + pod, ok := params.GetArguments()["pod"].(string) + if !ok || pod == "" { + p, err := getAnyRouterPod(params, defaultIngressControllerName) + if err != nil { + results = append(results, "# Router configuration") + results = append(results, fmt.Sprintf("Error getting router pod: %v", err)) + return api.NewToolCallResult(strings.Join(results, "\n"), nil), nil + } + pod = p + } + + out, err := kubernetes.NewCore(params).PodsExec(params.Context, ingressNamespace, pod, routerContainerName, []string{"cat", "/var/lib/haproxy/conf/haproxy.config"}) + if err != nil { + results = append(results, fmt.Sprintf("# Router configuration (pod: %s)", pod)) + results = append(results, fmt.Sprintf("Error showing router configuration from pod %q: %v", pod, err)) + } else { + results = append(results, fmt.Sprintf("# Router configuration (pod: %s)", pod)) + results = append(results, "```") + results = append(results, out) + results = append(results, "```") + } + + return api.NewToolCallResult(strings.Join(results, "\n"), nil), nil +} + +func getRouterInfo(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + var results []string + + pod, ok := params.GetArguments()["pod"].(string) + if !ok || pod == "" { + p, err := getAnyRouterPod(params, defaultIngressControllerName) + if err != nil { + results = append(results, "# Router HAProxy info") + results = append(results, fmt.Sprintf("Error getting router pod: %v", err)) + return api.NewToolCallResult(strings.Join(results, "\n"), nil), nil + } + pod = p + } + + out, err := kubernetes.NewCore(params).PodsExec(params.Context, ingressNamespace, pod, routerContainerName, []string{"sh", "-c", "echo 'show info' | socat stdio /var/lib/haproxy/run/haproxy.sock"}) + if err != nil { + results = append(results, fmt.Sprintf("# Router HAProxy info (pod: %s)", pod)) + results = append(results, fmt.Sprintf("Error getting HAProxy info from pod %q: %v", pod, err)) + } else { + results = append(results, fmt.Sprintf("# Router HAProxy info (pod: %s)", pod)) + results = append(results, "```") + results = append(results, out) + results = append(results, "```") + } + + return api.NewToolCallResult(strings.Join(results, "\n"), nil), nil +} + +func getRouterSessions(params api.ToolHandlerParams) (*api.ToolCallResult, error) { + var results []string + + pod, ok := params.GetArguments()["pod"].(string) + if !ok || pod == "" { + p, err := getAnyRouterPod(params, defaultIngressControllerName) + if err != nil { + results = append(results, "# Router active sessions") + results = append(results, fmt.Sprintf("Error getting router pod: %v", err)) + return api.NewToolCallResult(strings.Join(results, "\n"), nil), nil + } + pod = p + } + + out, err := kubernetes.NewCore(params).PodsExec(params.Context, ingressNamespace, pod, routerContainerName, []string{"sh", "-c", "echo 'show sess all' | socat stdio /var/lib/haproxy/run/haproxy.sock"}) + if err != nil { + results = append(results, fmt.Sprintf("# Router active sessions (pod: %s)", pod)) + results = append(results, fmt.Sprintf("Error getting active sessions from pod %q: %v", pod, err)) + } else { + results = append(results, fmt.Sprintf("# Router active sessions (pod: %s)", pod)) + results = append(results, "```") + results = append(results, out) + results = append(results, "```") + } + + return api.NewToolCallResult(strings.Join(results, "\n"), nil), nil +} + +func getAnyRouterPod(params api.ToolHandlerParams, icName string) (string, error) { + podGVK := &schema.GroupVersionKind{ + Group: "", + Version: "v1", + Kind: "Pod", + } + pods, err := kubernetes.NewCore(params).ResourcesList(params, podGVK, ingressNamespace, api.ListOptions{ + ListOptions: metav1.ListOptions{ + LabelSelector: "ingresscontroller.operator.openshift.io/deployment-ingresscontroller=" + icName, + }, + AsTable: false, + }) + if err != nil { + return "", fmt.Errorf("failed to list router pods: %v", err) + } + podsMap := pods.UnstructuredContent() + if items, ok := podsMap["items"].([]interface{}); ok { + for _, item := range items { + if itemMap, ok := item.(map[string]interface{}); ok { + if metadata, ok := itemMap["metadata"].(map[string]interface{}); ok { + if podName, ok := metadata["name"].(string); ok { + return podName, nil + } + } + } + } + } + return "", errors.New("no router pod found") +} diff --git a/pkg/toolsets/netedge/toolset.go b/pkg/toolsets/netedge/toolset.go index c3ed2c15a..b4d2526df 100644 --- a/pkg/toolsets/netedge/toolset.go +++ b/pkg/toolsets/netedge/toolset.go @@ -26,6 +26,7 @@ func (t *Toolset) GetTools(_ api.Openshift) []api.ServerTool { return slices.Concat( netedgeTools.InitQueryPrometheus(), initCoreDNS(), + initRouter(), ) }