mirror of
https://github.com/containers/kubernetes-mcp-server.git
synced 2025-10-23 01:22:57 +03:00
feat(pods): pods_top retrieves Pod resource consumption (metrics API) (119)
feat(pods): pods_top retrieves Pod resource consumption (metrics API) --- doc(pods): pods_top retrieves Pod resource consumption (metrics API)
This commit is contained in:
18
README.md
18
README.md
@@ -24,6 +24,7 @@ A powerful and flexible Kubernetes [Model Context Protocol (MCP)](https://blog.m
|
||||
- **Get** a pod by name from the specified namespace.
|
||||
- **Delete** a pod by name from the specified namespace.
|
||||
- **Show logs** for a pod by name from the specified namespace.
|
||||
- **Top** gets resource usage metrics for all pods or a specific pod in the specified namespace.
|
||||
- **Exec** into a pod and run a command.
|
||||
- **Run** a container image in a pod and optionally expose it.
|
||||
- **✅ Namespaces**: List Kubernetes Namespaces.
|
||||
@@ -314,6 +315,23 @@ Run a Kubernetes Pod in the current or provided namespace with the provided cont
|
||||
- TCP/IP port to expose from the Pod container
|
||||
- No port exposed if not provided
|
||||
|
||||
### `pods_top`
|
||||
|
||||
Lists the resource consumption (CPU and memory) as recorded by the Kubernetes Metrics Server for the specified Kubernetes Pods in the all namespaces, the provided namespace, or the current namespace
|
||||
|
||||
**Parameters:**
|
||||
- `all_namespaces` (`boolean`, optional, default: `true`)
|
||||
- If `true`, lists resource consumption for Pods in all namespaces
|
||||
- If `false`, lists resource consumption for Pods in the configured or provided namespace
|
||||
- `namespace` (`string`, optional)
|
||||
- Namespace to list the Pod resources from
|
||||
- If not provided, will list Pods from the configured namespace (in case all_namespaces is false)
|
||||
- `name` (`string`, optional)
|
||||
- Name of the Pod to get resource consumption from
|
||||
- If not provided, will list resource consumption for all Pods in the applicable namespace(s)
|
||||
- `label_selector` (`string`, optional)
|
||||
- Kubernetes label selector (e.g. 'app=myapp,env=prod' or 'app in (myapp,yourapp)'), use this option when you want to filter the pods by label (Optional, only applicable when name is not provided)
|
||||
|
||||
### `projects_list`
|
||||
|
||||
List all the OpenShift projects in the current cluster
|
||||
|
||||
3
go.mod
3
go.mod
@@ -18,6 +18,8 @@ require (
|
||||
k8s.io/cli-runtime v0.33.1
|
||||
k8s.io/client-go v0.33.1
|
||||
k8s.io/klog/v2 v2.130.1
|
||||
k8s.io/kubectl v0.33.0
|
||||
k8s.io/metrics v0.33.0
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
|
||||
sigs.k8s.io/controller-runtime v0.21.0
|
||||
sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20250211091558-894df3a7e664
|
||||
@@ -126,7 +128,6 @@ require (
|
||||
k8s.io/apiserver v0.33.1 // indirect
|
||||
k8s.io/component-base v0.33.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
|
||||
k8s.io/kubectl v0.33.0 // indirect
|
||||
oras.land/oras-go/v2 v2.5.0 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
|
||||
sigs.k8s.io/kustomize/api v0.19.0 // indirect
|
||||
|
||||
2
go.sum
2
go.sum
@@ -462,6 +462,8 @@ k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUy
|
||||
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
|
||||
k8s.io/kubectl v0.33.0 h1:HiRb1yqibBSCqic4pRZP+viiOBAnIdwYDpzUFejs07g=
|
||||
k8s.io/kubectl v0.33.0/go.mod h1:gAlGBuS1Jq1fYZ9AjGWbI/5Vk3M/VW2DK4g10Fpyn/0=
|
||||
k8s.io/metrics v0.33.0 h1:sKe5sC9qb1RakMhs8LWYNuN2ne6OTCWexj8Jos3rO2Y=
|
||||
k8s.io/metrics v0.33.0/go.mod h1:XewckTFXmE2AJiP7PT3EXaY7hi7bler3t2ZLyOdQYzU=
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
|
||||
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
oras.land/oras-go/v2 v2.5.0 h1:o8Me9kLY74Vp5uw07QXPiitjsw7qNXi8Twd+19Zf02c=
|
||||
|
||||
@@ -3,7 +3,11 @@ package kubernetes
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"k8s.io/metrics/pkg/apis/metrics"
|
||||
metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1"
|
||||
metricsclientset "k8s.io/metrics/pkg/client/clientset/versioned"
|
||||
|
||||
"github.com/manusa/kubernetes-mcp-server/pkg/version"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
@@ -18,6 +22,13 @@ import (
|
||||
"k8s.io/client-go/tools/remotecommand"
|
||||
)
|
||||
|
||||
type PodsTopOptions struct {
|
||||
metav1.ListOptions
|
||||
AllNamespaces bool
|
||||
Namespace string
|
||||
Name string
|
||||
}
|
||||
|
||||
func (k *Kubernetes) PodsListInAllNamespaces(ctx context.Context, options ResourceListOptions) (runtime.Unstructured, error) {
|
||||
return k.ResourcesList(ctx, &schema.GroupVersionKind{
|
||||
Group: "", Version: "v1", Kind: "Pod",
|
||||
@@ -175,6 +186,38 @@ func (k *Kubernetes) PodsRun(ctx context.Context, namespace, name, image string,
|
||||
return k.resourcesCreateOrUpdate(ctx, toCreate)
|
||||
}
|
||||
|
||||
func (k *Kubernetes) PodsTop(ctx context.Context, options PodsTopOptions) (*metrics.PodMetricsList, error) {
|
||||
// TODO, maybe move to mcp Tools setup and omit in case metrics aren't available in the target cluster
|
||||
if !k.supportsGroupVersion(metrics.GroupName + "/" + metricsv1beta1api.SchemeGroupVersion.Version) {
|
||||
return nil, errors.New("metrics API is not available")
|
||||
}
|
||||
namespace := options.Namespace
|
||||
if options.AllNamespaces && namespace == "" {
|
||||
namespace = ""
|
||||
} else {
|
||||
namespace = k.NamespaceOrDefault(namespace)
|
||||
}
|
||||
metricsClient, err := metricsclientset.NewForConfig(k.cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create metrics client: %w", err)
|
||||
}
|
||||
versionedMetrics := &metricsv1beta1api.PodMetricsList{}
|
||||
if options.Name != "" {
|
||||
m, err := metricsClient.MetricsV1beta1().PodMetricses(namespace).Get(ctx, options.Name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get metrics for pod %s/%s: %w", namespace, options.Name, err)
|
||||
}
|
||||
versionedMetrics.Items = []metricsv1beta1api.PodMetrics{*m}
|
||||
} else {
|
||||
versionedMetrics, err = metricsClient.MetricsV1beta1().PodMetricses(namespace).List(ctx, options.ListOptions)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list pod metrics in namespace %s: %w", namespace, err)
|
||||
}
|
||||
}
|
||||
convertedMetrics := &metrics.PodMetricsList{}
|
||||
return convertedMetrics, metricsv1beta1api.Convert_v1beta1_PodMetricsList_To_metrics_PodMetricsList(versionedMetrics, convertedMetrics, nil)
|
||||
}
|
||||
|
||||
func (k *Kubernetes) PodsExec(ctx context.Context, namespace, name, container string, command []string) (string, error) {
|
||||
namespace = k.NamespaceOrDefault(namespace)
|
||||
pod, err := k.clientSet.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{})
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/manusa/kubernetes-mcp-server/pkg/kubernetes"
|
||||
"github.com/manusa/kubernetes-mcp-server/pkg/output"
|
||||
"k8s.io/kubectl/pkg/metricsutil"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
@@ -53,6 +55,19 @@ func (s *Server) initPods() []server.ServerTool {
|
||||
mcp.WithIdempotentHintAnnotation(true),
|
||||
mcp.WithOpenWorldHintAnnotation(true),
|
||||
), Handler: s.podsDelete},
|
||||
{Tool: mcp.NewTool("pods_top",
|
||||
mcp.WithDescription("List the resource consumption (CPU and memory) as recorded by the Kubernetes Metrics Server for the specified Kubernetes Pods in the all namespaces, the provided namespace, or the current namespace"),
|
||||
mcp.WithBoolean("all_namespaces", mcp.Description("If true, list the resource consumption for all Pods in all namespaces. If false, list the resource consumption for Pods in the provided namespace or the current namespace"), mcp.DefaultBool(true)),
|
||||
mcp.WithString("namespace", mcp.Description("Namespace to get the Pods resource consumption from (Optional, current namespace if not provided and all_namespaces is false)")),
|
||||
mcp.WithString("name", mcp.Description("Name of the Pod to get the resource consumption from (Optional, all Pods in the namespace if not provided)")),
|
||||
mcp.WithString("label_selector", mcp.Description("Kubernetes label selector (e.g. 'app=myapp,env=prod' or 'app in (myapp,yourapp)'), use this option when you want to filter the pods by label (Optional, only applicable when name is not provided)"), mcp.Pattern("([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]")),
|
||||
// Tool annotations
|
||||
mcp.WithTitleAnnotation("Pods: Top"),
|
||||
mcp.WithReadOnlyHintAnnotation(true),
|
||||
mcp.WithDestructiveHintAnnotation(false),
|
||||
mcp.WithIdempotentHintAnnotation(true),
|
||||
mcp.WithOpenWorldHintAnnotation(true),
|
||||
), Handler: s.podsTop},
|
||||
{Tool: mcp.NewTool("pods_exec",
|
||||
mcp.WithDescription("Execute a command in a Kubernetes Pod in the current or provided namespace with the provided name and command"),
|
||||
mcp.WithString("namespace", mcp.Description("Namespace of the Pod where the command will be executed")),
|
||||
@@ -125,10 +140,10 @@ func (s *Server) podsListInNamespace(ctx context.Context, ctr mcp.CallToolReques
|
||||
if ns == nil {
|
||||
return NewTextResult("", errors.New("failed to list pods in namespace, missing argument namespace")), nil
|
||||
}
|
||||
labelSelector := ctr.GetArguments()["labelSelector"]
|
||||
resourceListOptions := kubernetes.ResourceListOptions{
|
||||
AsTable: s.configuration.ListOutput.AsTable(),
|
||||
}
|
||||
labelSelector := ctr.GetArguments()["labelSelector"]
|
||||
if labelSelector != nil {
|
||||
resourceListOptions.ListOptions.LabelSelector = labelSelector.(string)
|
||||
}
|
||||
@@ -171,6 +186,33 @@ func (s *Server) podsDelete(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.
|
||||
return NewTextResult(ret, err), nil
|
||||
}
|
||||
|
||||
func (s *Server) podsTop(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
podsTopOptions := kubernetes.PodsTopOptions{AllNamespaces: true}
|
||||
if v, ok := ctr.GetArguments()["namespace"].(string); ok {
|
||||
podsTopOptions.Namespace = v
|
||||
}
|
||||
if v, ok := ctr.GetArguments()["all_namespaces"].(bool); ok {
|
||||
podsTopOptions.AllNamespaces = v
|
||||
}
|
||||
if v, ok := ctr.GetArguments()["name"].(string); ok {
|
||||
podsTopOptions.Name = v
|
||||
}
|
||||
if v, ok := ctr.GetArguments()["label_selector"].(string); ok {
|
||||
podsTopOptions.LabelSelector = v
|
||||
}
|
||||
ret, err := s.k.Derived(ctx).PodsTop(ctx, podsTopOptions)
|
||||
if err != nil {
|
||||
return NewTextResult("", fmt.Errorf("failed to get pods top: %v", err)), nil
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
printer := metricsutil.NewTopCmdPrinter(buf)
|
||||
err = printer.PrintPodMetrics(ret.Items, true, true, false, "", true)
|
||||
if err != nil {
|
||||
return NewTextResult("", fmt.Errorf("failed to get pods top: %v", err)), nil
|
||||
}
|
||||
return NewTextResult(buf.String(), nil), nil
|
||||
}
|
||||
|
||||
func (s *Server) podsExec(ctx context.Context, ctr mcp.CallToolRequest) (*mcp.CallToolResult, error) {
|
||||
ns := ctr.GetArguments()["namespace"]
|
||||
if ns == nil {
|
||||
|
||||
@@ -97,6 +97,5 @@ func TestPodsExec(t *testing.T) {
|
||||
t.Errorf("expected container name not found %v", podsExecInNamespaceAndContainer.Content[0].(mcp.TextContent).Text)
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
195
pkg/mcp/pods_top_test.go
Normal file
195
pkg/mcp/pods_top_test.go
Normal file
@@ -0,0 +1,195 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPodsTop(t *testing.T) {
|
||||
testCase(t, func(c *mcpContext) {
|
||||
mockServer := NewMockServer()
|
||||
defer mockServer.Close()
|
||||
c.withKubeConfig(mockServer.config)
|
||||
metricsApiAvailable := false
|
||||
mockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
println("Request received:", req.Method, req.URL.Path) // TODO: REMOVE LINE
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
// Request Performed by DiscoveryClient to Kube API (Get API Groups legacy -core-)
|
||||
if req.URL.Path == "/api" {
|
||||
if !metricsApiAvailable {
|
||||
|
||||
_, _ = w.Write([]byte(`{"kind":"APIVersions","versions":[],"serverAddressByClientCIDRs":[{"clientCIDR":"0.0.0.0/0"}]}`))
|
||||
} else {
|
||||
_, _ = w.Write([]byte(`{"kind":"APIVersions","versions":["metrics.k8s.io/v1beta1"],"serverAddressByClientCIDRs":[{"clientCIDR":"0.0.0.0/0"}]}`))
|
||||
}
|
||||
return
|
||||
}
|
||||
// Request Performed by DiscoveryClient to Kube API (Get API Groups)
|
||||
if req.URL.Path == "/apis" {
|
||||
_, _ = w.Write([]byte(`{"kind":"APIGroupList","apiVersion":"v1","groups":[]}`))
|
||||
return
|
||||
}
|
||||
// Request Performed by DiscoveryClient to Kube API (Get API Resources)
|
||||
if metricsApiAvailable && req.URL.Path == "/apis/metrics.k8s.io/v1beta1" {
|
||||
_, _ = w.Write([]byte(`{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"metrics.k8s.io/v1beta1","resources":[{"name":"pods","singularName":"","namespaced":true,"kind":"PodMetrics","verbs":["get","list"]}]}`))
|
||||
return
|
||||
}
|
||||
// Pod Metrics from all namespaces
|
||||
if metricsApiAvailable && req.URL.Path == "/apis/metrics.k8s.io/v1beta1/pods" {
|
||||
if req.URL.Query().Get("labelSelector") == "app=pod-ns-5-42" {
|
||||
_, _ = w.Write([]byte(`{"kind":"PodMetricsList","apiVersion":"metrics.k8s.io/v1beta1","items":[` +
|
||||
`{"metadata":{"name":"pod-ns-5-42","namespace":"ns-5"},"containers":[{"name":"container-1","usage":{"cpu":"42m","memory":"42Mi"}}]}` +
|
||||
`]}`))
|
||||
} else {
|
||||
_, _ = w.Write([]byte(`{"kind":"PodMetricsList","apiVersion":"metrics.k8s.io/v1beta1","items":[` +
|
||||
`{"metadata":{"name":"pod-1","namespace":"default"},"containers":[{"name":"container-1","usage":{"cpu":"100m","memory":"200Mi"}},{"name":"container-2","usage":{"cpu":"200m","memory":"300Mi"}}]},` +
|
||||
`{"metadata":{"name":"pod-2","namespace":"ns-1"},"containers":[{"name":"container-1-ns-1","usage":{"cpu":"300m","memory":"400Mi"}}]}` +
|
||||
`]}`))
|
||||
|
||||
}
|
||||
return
|
||||
}
|
||||
// Pod Metrics from configured namespace
|
||||
if metricsApiAvailable && req.URL.Path == "/apis/metrics.k8s.io/v1beta1/namespaces/default/pods" {
|
||||
_, _ = w.Write([]byte(`{"kind":"PodMetricsList","apiVersion":"metrics.k8s.io/v1beta1","items":[` +
|
||||
`{"metadata":{"name":"pod-1","namespace":"default"},"containers":[{"name":"container-1","usage":{"cpu":"10m","memory":"20Mi"}},{"name":"container-2","usage":{"cpu":"30m","memory":"40Mi"}}]}` +
|
||||
`]}`))
|
||||
return
|
||||
}
|
||||
// Pod Metrics from ns-5 namespace
|
||||
if metricsApiAvailable && req.URL.Path == "/apis/metrics.k8s.io/v1beta1/namespaces/ns-5/pods" {
|
||||
_, _ = w.Write([]byte(`{"kind":"PodMetricsList","apiVersion":"metrics.k8s.io/v1beta1","items":[` +
|
||||
`{"metadata":{"name":"pod-ns-5-1","namespace":"ns-5"},"containers":[{"name":"container-1","usage":{"cpu":"10m","memory":"20Mi"}}]}` +
|
||||
`]}`))
|
||||
return
|
||||
}
|
||||
// Pod Metrics from ns-5 namespace with pod-ns-5-5 pod name
|
||||
if metricsApiAvailable && req.URL.Path == "/apis/metrics.k8s.io/v1beta1/namespaces/ns-5/pods/pod-ns-5-5" {
|
||||
_, _ = w.Write([]byte(`{"kind":"PodMetrics","apiVersion":"metrics.k8s.io/v1beta1",` +
|
||||
`"metadata":{"name":"pod-ns-5-5","namespace":"ns-5"},` +
|
||||
`"containers":[{"name":"container-1","usage":{"cpu":"13m","memory":"37Mi"}}]` +
|
||||
`}`))
|
||||
}
|
||||
}))
|
||||
podsTopMetricsApiUnavailable, err := c.callTool("pods_top", map[string]interface{}{})
|
||||
t.Run("pods_top with metrics API not available", func(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("call tool failed %v", err)
|
||||
}
|
||||
if !podsTopMetricsApiUnavailable.IsError {
|
||||
t.Errorf("call tool should have returned an error")
|
||||
}
|
||||
if podsTopMetricsApiUnavailable.Content[0].(mcp.TextContent).Text != "failed to get pods top: metrics API is not available" {
|
||||
t.Errorf("call tool returned unexpected content: %s", podsTopMetricsApiUnavailable.Content[0].(mcp.TextContent).Text)
|
||||
}
|
||||
})
|
||||
// Enable metrics API addon
|
||||
metricsApiAvailable = true
|
||||
d, _ := c.mcpServer.k.ToDiscoveryClient()
|
||||
d.Invalidate() // Force discovery client to refresh
|
||||
podsTopDefaults, err := c.callTool("pods_top", map[string]interface{}{})
|
||||
t.Run("pods_top defaults returns pod metrics from all namespaces", func(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("call tool failed %v", err)
|
||||
}
|
||||
textContent := podsTopDefaults.Content[0].(mcp.TextContent).Text
|
||||
if podsTopDefaults.IsError {
|
||||
t.Fatalf("call tool failed %s", textContent)
|
||||
}
|
||||
expectedHeaders := regexp.MustCompile("(?m)^\\s*NAMESPACE\\s+POD\\s+NAME\\s+CPU\\(cores\\)\\s+MEMORY\\(bytes\\)\\s*$")
|
||||
if !expectedHeaders.MatchString(textContent) {
|
||||
t.Errorf("Expected headers '%s' not found in output:\n%s", expectedHeaders.String(), textContent)
|
||||
}
|
||||
expectedRows := []string{
|
||||
"default\\s+pod-1\\s+container-1\\s+100m\\s+200Mi",
|
||||
"default\\s+pod-1\\s+container-2\\s+200m\\s+300Mi",
|
||||
"ns-1\\s+pod-2\\s+container-1-ns-1\\s+300m\\s+400Mi",
|
||||
}
|
||||
for _, row := range expectedRows {
|
||||
if !regexp.MustCompile(row).MatchString(textContent) {
|
||||
t.Errorf("Expected row '%s' not found in output:\n%s", row, textContent)
|
||||
}
|
||||
}
|
||||
expectedTotal := regexp.MustCompile("(?m)^\\s+600m\\s+900Mi\\s*$")
|
||||
if !expectedTotal.MatchString(textContent) {
|
||||
t.Errorf("Expected total row '%s' not found in output:\n%s", expectedTotal.String(), textContent)
|
||||
}
|
||||
})
|
||||
podsTopConfiguredNamespace, err := c.callTool("pods_top", map[string]interface{}{
|
||||
"all_namespaces": false,
|
||||
})
|
||||
t.Run("pods_top[allNamespaces=false] returns pod metrics from configured namespace", func(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("call tool failed %v", err)
|
||||
}
|
||||
textContent := podsTopConfiguredNamespace.Content[0].(mcp.TextContent).Text
|
||||
expectedRows := []string{
|
||||
"default\\s+pod-1\\s+container-1\\s+10m\\s+20Mi",
|
||||
"default\\s+pod-1\\s+container-2\\s+30m\\s+40Mi",
|
||||
}
|
||||
for _, row := range expectedRows {
|
||||
if !regexp.MustCompile(row).MatchString(textContent) {
|
||||
t.Errorf("Expected row '%s' not found in output:\n%s", row, textContent)
|
||||
}
|
||||
}
|
||||
expectedTotal := regexp.MustCompile("(?m)^\\s+40m\\s+60Mi\\s*$")
|
||||
if !expectedTotal.MatchString(textContent) {
|
||||
t.Errorf("Expected total row '%s' not found in output:\n%s", expectedTotal.String(), textContent)
|
||||
}
|
||||
})
|
||||
podsTopNamespace, err := c.callTool("pods_top", map[string]interface{}{
|
||||
"namespace": "ns-5",
|
||||
})
|
||||
t.Run("pods_top[namespace=ns-5] returns pod metrics from provided namespace", func(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("call tool failed %v", err)
|
||||
}
|
||||
textContent := podsTopNamespace.Content[0].(mcp.TextContent).Text
|
||||
expectedRow := regexp.MustCompile("ns-5\\s+pod-ns-5-1\\s+container-1\\s+10m\\s+20Mi")
|
||||
if !expectedRow.MatchString(textContent) {
|
||||
t.Errorf("Expected row '%s' not found in output:\n%s", expectedRow.String(), textContent)
|
||||
}
|
||||
expectedTotal := regexp.MustCompile("(?m)^\\s+10m\\s+20Mi\\s*$")
|
||||
if !expectedTotal.MatchString(textContent) {
|
||||
t.Errorf("Expected total row '%s' not found in output:\n%s", expectedTotal.String(), textContent)
|
||||
}
|
||||
})
|
||||
podsTopNamespaceName, err := c.callTool("pods_top", map[string]interface{}{
|
||||
"namespace": "ns-5",
|
||||
"name": "pod-ns-5-5",
|
||||
})
|
||||
t.Run("pods_top[namespace=ns-5,name=pod-ns-5-5] returns pod metrics from provided namespace and name", func(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("call tool failed %v", err)
|
||||
}
|
||||
textContent := podsTopNamespaceName.Content[0].(mcp.TextContent).Text
|
||||
expectedRow := regexp.MustCompile("ns-5\\s+pod-ns-5-5\\s+container-1\\s+13m\\s+37Mi")
|
||||
if !expectedRow.MatchString(textContent) {
|
||||
t.Errorf("Expected row '%s' not found in output:\n%s", expectedRow.String(), textContent)
|
||||
}
|
||||
expectedTotal := regexp.MustCompile("(?m)^\\s+13m\\s+37Mi\\s*$")
|
||||
if !expectedTotal.MatchString(textContent) {
|
||||
t.Errorf("Expected total row '%s' not found in output:\n%s", expectedTotal.String(), textContent)
|
||||
}
|
||||
})
|
||||
podsTopNamespaceLabelSelector, err := c.callTool("pods_top", map[string]interface{}{
|
||||
"label_selector": "app=pod-ns-5-42",
|
||||
})
|
||||
t.Run("pods_top[label_selector=app=pod-ns-5-42] returns pod metrics from pods matching selector", func(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("call tool failed %v", err)
|
||||
}
|
||||
textContent := podsTopNamespaceLabelSelector.Content[0].(mcp.TextContent).Text
|
||||
expectedRow := regexp.MustCompile("ns-5\\s+pod-ns-5-42\\s+container-1\\s+42m\\s+42Mi")
|
||||
if !expectedRow.MatchString(textContent) {
|
||||
t.Errorf("Expected row '%s' not found in output:\n%s", expectedRow.String(), textContent)
|
||||
}
|
||||
expectedTotal := regexp.MustCompile("(?m)^\\s+42m\\s+42Mi\\s*$")
|
||||
if !expectedTotal.MatchString(textContent) {
|
||||
t.Errorf("Expected total row '%s' not found in output:\n%s", expectedTotal.String(), textContent)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user