Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions pkg/kubernetes/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,16 +164,6 @@ func (m *Manager) Derived(ctx context.Context) *Kubernetes {
return derived
}

// TODO: check test to see why cache isn't getting invalidated automatically https://github.com/manusa/kubernetes-mcp-server/pull/125#discussion_r2152194784
func (k *Kubernetes) CacheInvalidate() {
if k.manager.discoveryClient != nil {
k.manager.discoveryClient.Invalidate()
}
if k.manager.deferredDiscoveryRESTMapper != nil {
k.manager.deferredDiscoveryRESTMapper.Reset()
}
}

func (k *Kubernetes) NewHelm() *helm.Helm {
// This is a derived Kubernetes, so it already has the Helm initialized
return helm.NewHelm(k.manager)
Expand Down
4 changes: 2 additions & 2 deletions pkg/mcp/namespaces_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func TestNamespacesListAsTable(t *testing.T) {
"(?<kind>Namespace)\\s+" +
"(?<name>ns-1)\\s+" +
"(?<status>Active)\\s+" +
"(?<age>\\d+(s|m))\\s+" +
"(?<age>(\\d+m)?\\d+s)\\s+" +
"(?<labels>kubernetes.io/metadata.name=ns-1)"
if m, e := regexp.MatchString(expectedRow, out); !m || e != nil {
t.Fatalf("Expected row '%s' not found in output:\n%s", expectedRow, out)
Expand All @@ -83,7 +83,7 @@ func TestNamespacesListAsTable(t *testing.T) {
"(?<kind>Namespace)\\s+" +
"(?<name>ns-2)\\s+" +
"(?<status>Active)\\s+" +
"(?<age>\\d+(s|m))\\s+" +
"(?<age>(\\d+m)?\\d+s)\\s+" +
"(?<labels>kubernetes.io/metadata.name=ns-2)"
if m, e := regexp.MatchString(expectedRow, out); !m || e != nil {
t.Fatalf("Expected row '%s' not found in output:\n%s", expectedRow, out)
Expand Down
6 changes: 3 additions & 3 deletions pkg/mcp/pods_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ func TestPodsListAsTable(t *testing.T) {
"(?<ready>0\\/1)\\s+" +
"(?<status>Pending)\\s+" +
"(?<restarts>0)\\s+" +
"(?<age>\\d+(s|m))\\s+" +
"(?<age>(\\d+m)?\\d+s)\\s+" +
"(?<ip><none>)\\s+" +
"(?<node><none>)\\s+" +
"(?<nominated_node><none>)\\s+" +
Expand All @@ -227,7 +227,7 @@ func TestPodsListAsTable(t *testing.T) {
"(?<ready>0\\/1)\\s+" +
"(?<status>Pending)\\s+" +
"(?<restarts>0)\\s+" +
"(?<age>\\d+(s|m))\\s+" +
"(?<age>(\\d+m)?\\d+s)\\s+" +
"(?<ip><none>)\\s+" +
"(?<node><none>)\\s+" +
"(?<nominated_node><none>)\\s+" +
Expand Down Expand Up @@ -270,7 +270,7 @@ func TestPodsListAsTable(t *testing.T) {
"(?<ready>0\\/1)\\s+" +
"(?<status>Pending)\\s+" +
"(?<restarts>0)\\s+" +
"(?<age>\\d+(s|m))\\s+" +
"(?<age>(\\d+m)?\\d+s)\\s+" +
"(?<ip><none>)\\s+" +
"(?<node><none>)\\s+" +
"(?<nominated_node><none>)\\s+" +
Expand Down
68 changes: 40 additions & 28 deletions pkg/mcp/pods_top_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,50 @@ import (
"testing"
)

func TestPodsTop(t *testing.T) {
func TestPodsTopMetricsUnavailable(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"}]}`))
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
}
}))
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)
}
})
})
}

_, _ = 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"}]}`))
}
func TestPodsTopMetricsAvailable(t *testing.T) {
testCase(t, func(c *mcpContext) {
mockServer := NewMockServer()
defer mockServer.Close()
c.withKubeConfig(mockServer.config)
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" {
_, _ = 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)
Expand All @@ -32,12 +59,12 @@ func TestPodsTop(t *testing.T) {
return
}
// Request Performed by DiscoveryClient to Kube API (Get API Resources)
if metricsApiAvailable && req.URL.Path == "/apis/metrics.k8s.io/v1beta1" {
if 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.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"}}]}` +
Expand All @@ -52,42 +79,27 @@ func TestPodsTop(t *testing.T) {
return
}
// Pod Metrics from configured namespace
if metricsApiAvailable && req.URL.Path == "/apis/metrics.k8s.io/v1beta1/namespaces/default/pods" {
if 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" {
if 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" {
if 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
c.mcpServer.k.Derived(t.Context()).CacheInvalidate() // 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 {
Expand Down
4 changes: 2 additions & 2 deletions pkg/mcp/resources_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ func TestResourcesListAsTable(t *testing.T) {
"(?<kind>ConfigMap)\\s+" +
"(?<name>a-configmap-to-list-as-table)\\s+" +
"(?<data>1)\\s+" +
"(?<age>\\d+(s|m))\\s+" +
"(?<age>(\\d+m)?\\d+s)\\s+" +
"(?<labels>resource=config-map)"
if m, e := regexp.MatchString(expectedRow, outConfigMapList); !m || e != nil {
t.Fatalf("Expected row '%s' not found in output:\n%s", expectedRow, outConfigMapList)
Expand Down Expand Up @@ -216,7 +216,7 @@ func TestResourcesListAsTable(t *testing.T) {
"(?<apiVersion>route.openshift.io/v1)\\s+" +
"(?<kind>Route)\\s+" +
"(?<name>an-openshift-route-to-list-as-table)\\s+" +
"(?<age>\\d+(s|m))\\s+" +
"(?<age>(\\d+m)?\\d+s)\\s+" +
"(?<labels><none>)"
if m, e := regexp.MatchString(expectedRow, outRouteList); !m || e != nil {
t.Fatalf("Expected row '%s' not found in output:\n%s", expectedRow, outRouteList)
Expand Down
Loading