Skip to content

Commit 9a6ebe8

Browse files
committed
feat(config): provide a limited metrics clientset to check access
1 parent 8a0681a commit 9a6ebe8

File tree

3 files changed

+84
-26
lines changed

3 files changed

+84
-26
lines changed

pkg/kubernetes/accesscontrol_clientset.go

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
package kubernetes
22

33
import (
4+
"context"
5+
"fmt"
6+
47
authorizationv1api "k8s.io/api/authorization/v1"
8+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
59
"k8s.io/apimachinery/pkg/runtime/schema"
610
"k8s.io/client-go/discovery"
711
"k8s.io/client-go/kubernetes"
812
authorizationv1 "k8s.io/client-go/kubernetes/typed/authorization/v1"
913
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
1014
"k8s.io/client-go/rest"
15+
"k8s.io/metrics/pkg/apis/metrics"
16+
metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1"
17+
metricsv1beta1 "k8s.io/metrics/pkg/client/clientset/versioned/typed/metrics/v1beta1"
1118

1219
"github.com/manusa/kubernetes-mcp-server/pkg/config"
1320
)
@@ -18,6 +25,7 @@ import (
1825
type AccessControlClientset struct {
1926
delegate kubernetes.Interface
2027
discoveryClient discovery.DiscoveryInterface
28+
metricsV1beta1 *metricsv1beta1.MetricsV1beta1Client
2129
staticConfig *config.StaticConfig // TODO: maybe just store the denied resource slice
2230
}
2331

@@ -47,6 +55,29 @@ func (a *AccessControlClientset) PodsExec(namespace, name string) (*rest.Request
4755
SubResource("exec"), nil
4856
}
4957

58+
func (a *AccessControlClientset) PodsMetricses(ctx context.Context, namespace, name string, listOptions metav1.ListOptions) (*metrics.PodMetricsList, error) {
59+
gvk := &schema.GroupVersionKind{Group: metrics.GroupName, Version: metricsv1beta1api.SchemeGroupVersion.Version, Kind: "PodMetrics"}
60+
if !isAllowed(a.staticConfig, gvk) {
61+
return nil, isNotAllowedError(gvk)
62+
}
63+
versionedMetrics := &metricsv1beta1api.PodMetricsList{}
64+
var err error
65+
if name != "" {
66+
m, err := a.metricsV1beta1.PodMetricses(namespace).Get(ctx, name, metav1.GetOptions{})
67+
if err != nil {
68+
return nil, fmt.Errorf("failed to get metrics for pod %s/%s: %w", namespace, name, err)
69+
}
70+
versionedMetrics.Items = []metricsv1beta1api.PodMetrics{*m}
71+
} else {
72+
versionedMetrics, err = a.metricsV1beta1.PodMetricses(namespace).List(ctx, listOptions)
73+
if err != nil {
74+
return nil, fmt.Errorf("failed to list pod metrics in namespace %s: %w", namespace, err)
75+
}
76+
}
77+
convertedMetrics := &metrics.PodMetricsList{}
78+
return convertedMetrics, metricsv1beta1api.Convert_v1beta1_PodMetricsList_To_metrics_PodMetricsList(versionedMetrics, convertedMetrics, nil)
79+
}
80+
5081
func (a *AccessControlClientset) Services(namespace string) (corev1.ServiceInterface, error) {
5182
gvk := &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Service"}
5283
if !isAllowed(a.staticConfig, gvk) {
@@ -68,7 +99,14 @@ func NewAccessControlClientset(cfg *rest.Config, staticConfig *config.StaticConf
6899
if err != nil {
69100
return nil, err
70101
}
102+
metricsClient, err := metricsv1beta1.NewForConfig(cfg)
103+
if err != nil {
104+
return nil, err
105+
}
71106
return &AccessControlClientset{
72-
delegate: clientSet, discoveryClient: clientSet.DiscoveryClient, staticConfig: staticConfig,
107+
delegate: clientSet,
108+
discoveryClient: clientSet.DiscoveryClient,
109+
metricsV1beta1: metricsClient,
110+
staticConfig: staticConfig,
73111
}, nil
74112
}

pkg/kubernetes/pods.go

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@ import (
55
"context"
66
"errors"
77
"fmt"
8-
"k8s.io/metrics/pkg/apis/metrics"
9-
metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1"
10-
metricsclientset "k8s.io/metrics/pkg/client/clientset/versioned"
118

12-
"github.com/manusa/kubernetes-mcp-server/pkg/version"
139
v1 "k8s.io/api/core/v1"
1410
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1511
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -20,6 +16,10 @@ import (
2016
"k8s.io/apimachinery/pkg/util/intstr"
2117
"k8s.io/apimachinery/pkg/util/rand"
2218
"k8s.io/client-go/tools/remotecommand"
19+
"k8s.io/metrics/pkg/apis/metrics"
20+
metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1"
21+
22+
"github.com/manusa/kubernetes-mcp-server/pkg/version"
2323
)
2424

2525
type PodsTopOptions struct {
@@ -205,25 +205,7 @@ func (k *Kubernetes) PodsTop(ctx context.Context, options PodsTopOptions) (*metr
205205
} else {
206206
namespace = k.NamespaceOrDefault(namespace)
207207
}
208-
metricsClient, err := metricsclientset.NewForConfig(k.manager.cfg)
209-
if err != nil {
210-
return nil, fmt.Errorf("failed to create metrics client: %w", err)
211-
}
212-
versionedMetrics := &metricsv1beta1api.PodMetricsList{}
213-
if options.Name != "" {
214-
m, err := metricsClient.MetricsV1beta1().PodMetricses(namespace).Get(ctx, options.Name, metav1.GetOptions{})
215-
if err != nil {
216-
return nil, fmt.Errorf("failed to get metrics for pod %s/%s: %w", namespace, options.Name, err)
217-
}
218-
versionedMetrics.Items = []metricsv1beta1api.PodMetrics{*m}
219-
} else {
220-
versionedMetrics, err = metricsClient.MetricsV1beta1().PodMetricses(namespace).List(ctx, options.ListOptions)
221-
if err != nil {
222-
return nil, fmt.Errorf("failed to list pod metrics in namespace %s: %w", namespace, err)
223-
}
224-
}
225-
convertedMetrics := &metrics.PodMetricsList{}
226-
return convertedMetrics, metricsv1beta1api.Convert_v1beta1_PodMetricsList_To_metrics_PodMetricsList(versionedMetrics, convertedMetrics, nil)
208+
return k.manager.accessControlClientSet.PodsMetricses(ctx, namespace, options.Name, options.ListOptions)
227209
}
228210

229211
func (k *Kubernetes) PodsExec(ctx context.Context, namespace, name, container string, command []string) (string, error) {

pkg/mcp/pods_top_test.go

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package mcp
22

33
import (
4-
"github.com/mark3labs/mcp-go/mcp"
54
"net/http"
65
"regexp"
76
"testing"
7+
8+
"github.com/mark3labs/mcp-go/mcp"
9+
10+
"github.com/manusa/kubernetes-mcp-server/pkg/config"
811
)
912

1013
func TestPodsTopMetricsUnavailable(t *testing.T) {
@@ -206,5 +209,40 @@ func TestPodsTopMetricsAvailable(t *testing.T) {
206209
}
207210

208211
func TestPodsTopDenied(t *testing.T) {
209-
t.Skip("To be implemented") // TODO: top is not checking for denied resources
212+
deniedResourcesServer := &config.StaticConfig{DeniedResources: []config.GroupVersionKind{{Group: "metrics.k8s.io", Version: "v1beta1"}}}
213+
testCaseWithContext(t, &mcpContext{staticConfig: deniedResourcesServer}, func(c *mcpContext) {
214+
mockServer := NewMockServer()
215+
defer mockServer.Close()
216+
c.withKubeConfig(mockServer.config)
217+
mockServer.Handle(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
218+
w.Header().Set("Content-Type", "application/json")
219+
// Request Performed by DiscoveryClient to Kube API (Get API Groups legacy -core-)
220+
if req.URL.Path == "/api" {
221+
_, _ = w.Write([]byte(`{"kind":"APIVersions","versions":["metrics.k8s.io/v1beta1"],"serverAddressByClientCIDRs":[{"clientCIDR":"0.0.0.0/0"}]}`))
222+
return
223+
}
224+
// Request Performed by DiscoveryClient to Kube API (Get API Groups)
225+
if req.URL.Path == "/apis" {
226+
_, _ = w.Write([]byte(`{"kind":"APIGroupList","apiVersion":"v1","groups":[]}`))
227+
return
228+
}
229+
// Request Performed by DiscoveryClient to Kube API (Get API Resources)
230+
if req.URL.Path == "/apis/metrics.k8s.io/v1beta1" {
231+
_, _ = w.Write([]byte(`{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"metrics.k8s.io/v1beta1","resources":[{"name":"pods","singularName":"","namespaced":true,"kind":"PodMetrics","verbs":["get","list"]}]}`))
232+
return
233+
}
234+
}))
235+
podsTop, _ := c.callTool("pods_top", map[string]interface{}{})
236+
t.Run("pods_run has error", func(t *testing.T) {
237+
if !podsTop.IsError {
238+
t.Fatalf("call tool should fail")
239+
}
240+
})
241+
t.Run("pods_run describes denial", func(t *testing.T) {
242+
expectedMessage := "failed to get pods top: resource not allowed: metrics.k8s.io/v1beta1, Kind=PodMetrics"
243+
if podsTop.Content[0].(mcp.TextContent).Text != expectedMessage {
244+
t.Fatalf("expected descriptive error '%s', got %v", expectedMessage, podsTop.Content[0].(mcp.TextContent).Text)
245+
}
246+
})
247+
})
210248
}

0 commit comments

Comments
 (0)