diff --git a/pkg/kubernetes/kubernetes.go b/pkg/kubernetes/kubernetes.go index cb69a574..742ede5d 100644 --- a/pkg/kubernetes/kubernetes.go +++ b/pkg/kubernetes/kubernetes.go @@ -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) diff --git a/pkg/mcp/namespaces_test.go b/pkg/mcp/namespaces_test.go index 0bb862a5..ab6f8bea 100644 --- a/pkg/mcp/namespaces_test.go +++ b/pkg/mcp/namespaces_test.go @@ -72,7 +72,7 @@ func TestNamespacesListAsTable(t *testing.T) { "(?Namespace)\\s+" + "(?ns-1)\\s+" + "(?Active)\\s+" + - "(?\\d+(s|m))\\s+" + + "(?(\\d+m)?\\d+s)\\s+" + "(?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) @@ -83,7 +83,7 @@ func TestNamespacesListAsTable(t *testing.T) { "(?Namespace)\\s+" + "(?ns-2)\\s+" + "(?Active)\\s+" + - "(?\\d+(s|m))\\s+" + + "(?(\\d+m)?\\d+s)\\s+" + "(?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) diff --git a/pkg/mcp/pods_test.go b/pkg/mcp/pods_test.go index d96dd8d3..18dc1e8b 100644 --- a/pkg/mcp/pods_test.go +++ b/pkg/mcp/pods_test.go @@ -209,7 +209,7 @@ func TestPodsListAsTable(t *testing.T) { "(?0\\/1)\\s+" + "(?Pending)\\s+" + "(?0)\\s+" + - "(?\\d+(s|m))\\s+" + + "(?(\\d+m)?\\d+s)\\s+" + "(?)\\s+" + "(?)\\s+" + "(?)\\s+" + @@ -227,7 +227,7 @@ func TestPodsListAsTable(t *testing.T) { "(?0\\/1)\\s+" + "(?Pending)\\s+" + "(?0)\\s+" + - "(?\\d+(s|m))\\s+" + + "(?(\\d+m)?\\d+s)\\s+" + "(?)\\s+" + "(?)\\s+" + "(?)\\s+" + @@ -270,7 +270,7 @@ func TestPodsListAsTable(t *testing.T) { "(?0\\/1)\\s+" + "(?Pending)\\s+" + "(?0)\\s+" + - "(?\\d+(s|m))\\s+" + + "(?(\\d+m)?\\d+s)\\s+" + "(?)\\s+" + "(?)\\s+" + "(?)\\s+" + diff --git a/pkg/mcp/pods_top_test.go b/pkg/mcp/pods_top_test.go index 4e288a23..dcec9a22 100644 --- a/pkg/mcp/pods_top_test.go +++ b/pkg/mcp/pods_top_test.go @@ -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) @@ -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"}}]}` + @@ -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 { diff --git a/pkg/mcp/resources_test.go b/pkg/mcp/resources_test.go index f6a938cb..0e09fcae 100644 --- a/pkg/mcp/resources_test.go +++ b/pkg/mcp/resources_test.go @@ -178,7 +178,7 @@ func TestResourcesListAsTable(t *testing.T) { "(?ConfigMap)\\s+" + "(?a-configmap-to-list-as-table)\\s+" + "(?1)\\s+" + - "(?\\d+(s|m))\\s+" + + "(?(\\d+m)?\\d+s)\\s+" + "(?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) @@ -216,7 +216,7 @@ func TestResourcesListAsTable(t *testing.T) { "(?route.openshift.io/v1)\\s+" + "(?Route)\\s+" + "(?an-openshift-route-to-list-as-table)\\s+" + - "(?\\d+(s|m))\\s+" + + "(?(\\d+m)?\\d+s)\\s+" + "(?)" if m, e := regexp.MatchString(expectedRow, outRouteList); !m || e != nil { t.Fatalf("Expected row '%s' not found in output:\n%s", expectedRow, outRouteList)