Skip to content

Commit ed3059a

Browse files
committed
wip:feat(kubernetes)!: simplified Kubernetes client access for toolsets
Signed-off-by: Marc Nuri <[email protected]>
1 parent 4f69d09 commit ed3059a

13 files changed

+141
-190
lines changed

internal/test/mock_server.go

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ package test
33
import (
44
"encoding/json"
55
"errors"
6+
"fmt"
67
"io"
78
"net/http"
89
"net/http/httptest"
910
"path/filepath"
11+
"strings"
1012
"testing"
1113

1214
"github.com/stretchr/testify/require"
@@ -186,7 +188,10 @@ WaitForStreams:
186188
return ctx, nil
187189
}
188190

189-
type DiscoveryClientHandler struct{}
191+
type DiscoveryClientHandler struct {
192+
V1Resources []string
193+
Groups []string
194+
}
190195

191196
var _ http.Handler = (*DiscoveryClientHandler)(nil)
192197

@@ -200,17 +205,18 @@ func (h *DiscoveryClientHandler) ServeHTTP(w http.ResponseWriter, req *http.Requ
200205
// Request Performed by DiscoveryClient to Kube API (Get API Groups)
201206
if req.URL.Path == "/apis" {
202207
w.Header().Set("Content-Type", "application/json")
203-
_, _ = w.Write([]byte(`{"kind":"APIGroupList","apiVersion":"v1","groups":[
204-
{"name":"apps","versions":[{"groupVersion":"apps/v1","version":"v1"}],"preferredVersion":{"groupVersion":"apps/v1","version":"v1"}}
205-
]}`))
208+
_, _ = fmt.Fprintf(w, `{"kind":"APIGroupList","apiVersion":"v1","groups":[%s]}`, strings.Join(append(h.Groups,
209+
`{"name":"apps","versions":[{"groupVersion":"apps/v1","version":"v1"}],"preferredVersion":{"groupVersion":"apps/v1","version":"v1"}}`,
210+
), ","))
206211
return
207212
}
208213
// Request Performed by DiscoveryClient to Kube API (Get API Resources)
209214
if req.URL.Path == "/api/v1" {
210215
w.Header().Set("Content-Type", "application/json")
211-
_, _ = w.Write([]byte(`{"kind":"APIResourceList","apiVersion":"v1","resources":[
212-
{"name":"pods","singularName":"","namespaced":true,"kind":"Pod","verbs":["get","list","watch","create","update","patch","delete"]}
213-
]}`))
216+
_, _ = fmt.Fprintf(w, `{"kind":"APIResourceList","apiVersion":"v1","resources":[%s]}`, strings.Join(append(h.V1Resources,
217+
`{"name":"nodes","singularName":"","namespaced":false,"kind":"Node","verbs":["get","list","watch"]}`,
218+
`{"name":"pods","singularName":"","namespaced":true,"kind":"Pod","verbs":["get","list","watch","create","update","patch","delete"]}`,
219+
), ","))
214220
return
215221
}
216222
if req.URL.Path == "/apis/apps/v1" {
@@ -273,12 +279,27 @@ const tokenReviewSuccessful = `
273279
}`
274280

275281
type TokenReviewHandler struct {
282+
DiscoveryClientHandler
276283
TokenReviewed bool
277284
}
278285

279286
var _ http.Handler = (*TokenReviewHandler)(nil)
280287

288+
func NewTokenReviewHandler() *TokenReviewHandler {
289+
trh := &TokenReviewHandler{}
290+
trh.DiscoveryClientHandler.Groups = []string{
291+
`{"name":"authentication.k8s.io","versions":[{"groupVersion":"authentication.k8s.io/v1","version":"v1"}],"preferredVersion":{"groupVersion":"authentication.k8s.io/v1","version":"v1"}}`,
292+
}
293+
return trh
294+
}
295+
281296
func (h *TokenReviewHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
297+
h.DiscoveryClientHandler.ServeHTTP(w, req)
298+
if req.URL.EscapedPath() == "/apis/authentication.k8s.io/v1" {
299+
w.Header().Set("Content-Type", "application/json")
300+
_, _ = w.Write([]byte(`{"kind":"APIResourceList","apiVersion":"v1","groupVersion":"authentication.k8s.io/v1","resources":[{"name":"tokenreviews","singularName":"","namespaced":false,"kind":"TokenReview","verbs":["create"]}]}`))
301+
return
302+
}
282303
if req.URL.EscapedPath() == "/apis/authentication.k8s.io/v1/tokenreviews" {
283304
w.Header().Set("Content-Type", "application/json")
284305
_, _ = w.Write([]byte(tokenReviewSuccessful))

pkg/http/http_authorization_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ func (s *AuthorizationSuite) TestAuthorizationRequireOAuthFalse() {
324324
}
325325

326326
func (s *AuthorizationSuite) TestAuthorizationRawToken() {
327-
tokenReviewHandler := &test.TokenReviewHandler{}
327+
tokenReviewHandler := test.NewTokenReviewHandler()
328328
s.MockServer.Handle(tokenReviewHandler)
329329

330330
cases := []struct {
@@ -367,7 +367,7 @@ func (s *AuthorizationSuite) TestAuthorizationRawToken() {
367367
}
368368

369369
func (s *AuthorizationSuite) TestAuthorizationOidcToken() {
370-
tokenReviewHandler := &test.TokenReviewHandler{}
370+
tokenReviewHandler := test.NewTokenReviewHandler()
371371
s.MockServer.Handle(tokenReviewHandler)
372372

373373
oidcTestServer := NewOidcTestServer(s.T())
@@ -412,7 +412,7 @@ func (s *AuthorizationSuite) TestAuthorizationOidcToken() {
412412
}
413413

414414
func (s *AuthorizationSuite) TestAuthorizationOidcTokenExchange() {
415-
tokenReviewHandler := &test.TokenReviewHandler{}
415+
tokenReviewHandler := test.NewTokenReviewHandler()
416416
s.MockServer.Handle(tokenReviewHandler)
417417

418418
oidcTestServer := NewOidcTestServer(s.T())

pkg/kubernetes/accesscontrol_clientset.go

Lines changed: 72 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -3,71 +3,103 @@ package kubernetes
33
import (
44
"context"
55
"fmt"
6+
"net/http"
67

7-
authenticationv1api "k8s.io/api/authentication/v1"
8-
authorizationv1api "k8s.io/api/authorization/v1"
8+
"github.com/containers/kubernetes-mcp-server/pkg/config"
99
v1 "k8s.io/api/core/v1"
10+
"k8s.io/apimachinery/pkg/api/meta"
1011
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11-
"k8s.io/apimachinery/pkg/runtime/schema"
1212
"k8s.io/apimachinery/pkg/util/httpstream"
1313
"k8s.io/client-go/discovery"
14+
"k8s.io/client-go/discovery/cached/memory"
15+
"k8s.io/client-go/dynamic"
1416
"k8s.io/client-go/kubernetes"
1517
authenticationv1 "k8s.io/client-go/kubernetes/typed/authentication/v1"
1618
authorizationv1 "k8s.io/client-go/kubernetes/typed/authorization/v1"
1719
corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
1820
"k8s.io/client-go/rest"
21+
"k8s.io/client-go/restmapper"
1922
"k8s.io/client-go/tools/remotecommand"
2023
"k8s.io/metrics/pkg/apis/metrics"
2124
metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1"
2225
metricsv1beta1 "k8s.io/metrics/pkg/client/clientset/versioned/typed/metrics/v1beta1"
23-
24-
"github.com/containers/kubernetes-mcp-server/pkg/config"
2526
)
2627

2728
// AccessControlClientset is a limited clientset delegating interface to the standard kubernetes.Clientset
2829
// Only a limited set of functions are implemented with a single point of access to the kubernetes API where
2930
// apiVersion and kinds are checked for allowed access
3031
type AccessControlClientset struct {
31-
cfg *rest.Config
32-
delegate kubernetes.Interface
33-
discoveryClient discovery.DiscoveryInterface
32+
cfg *rest.Config
33+
kubernetes.Interface
34+
discoveryClient discovery.CachedDiscoveryInterface
35+
dynamicClient *dynamic.DynamicClient
36+
restMapper meta.ResettableRESTMapper
3437
metricsV1beta1 *metricsv1beta1.MetricsV1beta1Client
35-
staticConfig *config.StaticConfig // TODO: maybe just store the denied resource slice
3638
}
3739

38-
func (a *AccessControlClientset) DiscoveryClient() discovery.DiscoveryInterface {
40+
func NewAccessControlClientset(staticConfig *config.StaticConfig, restConfig *rest.Config) (*AccessControlClientset, error) {
41+
configShallowCopy := &(*restConfig)
42+
acc := &AccessControlClientset{
43+
cfg: configShallowCopy,
44+
}
45+
discoveryClient, err := discovery.NewDiscoveryClientForConfig(acc.cfg)
46+
if err != nil {
47+
return nil, fmt.Errorf("failed to create discovery client: %v", err)
48+
}
49+
acc.discoveryClient = memory.NewMemCacheClient(discoveryClient)
50+
acc.restMapper = restmapper.NewDeferredDiscoveryRESTMapper(acc.discoveryClient)
51+
acc.cfg.Wrap(func(original http.RoundTripper) http.RoundTripper {
52+
return &AccessControlRoundTripper{
53+
delegate: original,
54+
staticConfig: staticConfig,
55+
restMapper: acc.restMapper,
56+
}
57+
})
58+
acc.Interface, err = kubernetes.NewForConfig(acc.cfg)
59+
if err != nil {
60+
return nil, err
61+
}
62+
acc.dynamicClient, err = dynamic.NewForConfig(acc.cfg)
63+
if err != nil {
64+
return nil, err
65+
}
66+
acc.metricsV1beta1, err = metricsv1beta1.NewForConfig(acc.cfg)
67+
if err != nil {
68+
return nil, err
69+
}
70+
return acc, nil
71+
}
72+
73+
func (a *AccessControlClientset) DiscoveryClient() discovery.CachedDiscoveryInterface {
3974
return a.discoveryClient
4075
}
4176

77+
func (a *AccessControlClientset) DynamicClient() dynamic.Interface {
78+
return a.dynamicClient
79+
}
80+
81+
func (a *AccessControlClientset) RESTMapper() meta.ResettableRESTMapper {
82+
return a.restMapper
83+
}
84+
85+
// Nodes returns NodeInterface
86+
// Deprecated: use CoreV1().Nodes() directly
4287
func (a *AccessControlClientset) Nodes() (corev1.NodeInterface, error) {
43-
gvk := &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Node"}
44-
if !isAllowed(a.staticConfig, gvk) {
45-
return nil, isNotAllowedError(gvk)
46-
}
47-
return a.delegate.CoreV1().Nodes(), nil
88+
return a.CoreV1().Nodes(), nil
4889
}
4990

5091
func (a *AccessControlClientset) NodesLogs(ctx context.Context, name string) (*rest.Request, error) {
51-
gvk := &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Node"}
52-
if !isAllowed(a.staticConfig, gvk) {
53-
return nil, isNotAllowedError(gvk)
54-
}
55-
56-
if _, err := a.delegate.CoreV1().Nodes().Get(ctx, name, metav1.GetOptions{}); err != nil {
92+
if _, err := a.CoreV1().Nodes().Get(ctx, name, metav1.GetOptions{}); err != nil {
5793
return nil, fmt.Errorf("failed to get node %s: %w", name, err)
5894
}
5995

6096
url := []string{"api", "v1", "nodes", name, "proxy", "logs"}
61-
return a.delegate.CoreV1().RESTClient().
97+
return a.CoreV1().RESTClient().
6298
Get().
6399
AbsPath(url...), nil
64100
}
65101

66102
func (a *AccessControlClientset) NodesMetricses(ctx context.Context, name string, listOptions metav1.ListOptions) (*metrics.NodeMetricsList, error) {
67-
gvk := &schema.GroupVersionKind{Group: metrics.GroupName, Version: metricsv1beta1api.SchemeGroupVersion.Version, Kind: "NodeMetrics"}
68-
if !isAllowed(a.staticConfig, gvk) {
69-
return nil, isNotAllowedError(gvk)
70-
}
71103
versionedMetrics := &metricsv1beta1api.NodeMetricsList{}
72104
var err error
73105
if name != "" {
@@ -87,37 +119,26 @@ func (a *AccessControlClientset) NodesMetricses(ctx context.Context, name string
87119
}
88120

89121
func (a *AccessControlClientset) NodesStatsSummary(ctx context.Context, name string) (*rest.Request, error) {
90-
gvk := &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Node"}
91-
if !isAllowed(a.staticConfig, gvk) {
92-
return nil, isNotAllowedError(gvk)
93-
}
94-
95-
if _, err := a.delegate.CoreV1().Nodes().Get(ctx, name, metav1.GetOptions{}); err != nil {
122+
if _, err := a.CoreV1().Nodes().Get(ctx, name, metav1.GetOptions{}); err != nil {
96123
return nil, fmt.Errorf("failed to get node %s: %w", name, err)
97124
}
98125

99126
url := []string{"api", "v1", "nodes", name, "proxy", "stats", "summary"}
100-
return a.delegate.CoreV1().RESTClient().
127+
return a.CoreV1().RESTClient().
101128
Get().
102129
AbsPath(url...), nil
103130
}
104131

132+
// Pods returns PodInterface
133+
// Deprecated: use CoreV1().Pods(namespace) directly
105134
func (a *AccessControlClientset) Pods(namespace string) (corev1.PodInterface, error) {
106-
gvk := &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}
107-
if !isAllowed(a.staticConfig, gvk) {
108-
return nil, isNotAllowedError(gvk)
109-
}
110-
return a.delegate.CoreV1().Pods(namespace), nil
135+
return a.CoreV1().Pods(namespace), nil
111136
}
112137

113138
func (a *AccessControlClientset) PodsExec(namespace, name string, podExecOptions *v1.PodExecOptions) (remotecommand.Executor, error) {
114-
gvk := &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"}
115-
if !isAllowed(a.staticConfig, gvk) {
116-
return nil, isNotAllowedError(gvk)
117-
}
118139
// Compute URL
119140
// https://github.com/kubernetes/kubectl/blob/5366de04e168bcbc11f5e340d131a9ca8b7d0df4/pkg/cmd/exec/exec.go#L382-L397
120-
execRequest := a.delegate.CoreV1().RESTClient().
141+
execRequest := a.CoreV1().RESTClient().
121142
Post().
122143
Resource("pods").
123144
Namespace(namespace).
@@ -138,10 +159,6 @@ func (a *AccessControlClientset) PodsExec(namespace, name string, podExecOptions
138159
}
139160

140161
func (a *AccessControlClientset) PodsMetricses(ctx context.Context, namespace, name string, listOptions metav1.ListOptions) (*metrics.PodMetricsList, error) {
141-
gvk := &schema.GroupVersionKind{Group: metrics.GroupName, Version: metricsv1beta1api.SchemeGroupVersion.Version, Kind: "PodMetrics"}
142-
if !isAllowed(a.staticConfig, gvk) {
143-
return nil, isNotAllowedError(gvk)
144-
}
145162
versionedMetrics := &metricsv1beta1api.PodMetricsList{}
146163
var err error
147164
if name != "" {
@@ -160,45 +177,20 @@ func (a *AccessControlClientset) PodsMetricses(ctx context.Context, namespace, n
160177
return convertedMetrics, metricsv1beta1api.Convert_v1beta1_PodMetricsList_To_metrics_PodMetricsList(versionedMetrics, convertedMetrics, nil)
161178
}
162179

180+
// Services returns ServiceInterface
181+
// Deprecated: use CoreV1().Services(namespace) directly
163182
func (a *AccessControlClientset) Services(namespace string) (corev1.ServiceInterface, error) {
164-
gvk := &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Service"}
165-
if !isAllowed(a.staticConfig, gvk) {
166-
return nil, isNotAllowedError(gvk)
167-
}
168-
return a.delegate.CoreV1().Services(namespace), nil
183+
return a.CoreV1().Services(namespace), nil
169184
}
170185

186+
// SelfSubjectAccessReviews returns SelfSubjectAccessReviewInterface
187+
// Deprecated: use AuthorizationV1().SelfSubjectAccessReviews() directly
171188
func (a *AccessControlClientset) SelfSubjectAccessReviews() (authorizationv1.SelfSubjectAccessReviewInterface, error) {
172-
gvk := &schema.GroupVersionKind{Group: authorizationv1api.GroupName, Version: authorizationv1api.SchemeGroupVersion.Version, Kind: "SelfSubjectAccessReview"}
173-
if !isAllowed(a.staticConfig, gvk) {
174-
return nil, isNotAllowedError(gvk)
175-
}
176-
return a.delegate.AuthorizationV1().SelfSubjectAccessReviews(), nil
189+
return a.AuthorizationV1().SelfSubjectAccessReviews(), nil
177190
}
178191

179192
// TokenReview returns TokenReviewInterface
193+
// Deprecated: use AuthenticationV1().TokenReviews() directly
180194
func (a *AccessControlClientset) TokenReview() (authenticationv1.TokenReviewInterface, error) {
181-
gvk := &schema.GroupVersionKind{Group: authenticationv1api.GroupName, Version: authorizationv1api.SchemeGroupVersion.Version, Kind: "TokenReview"}
182-
if !isAllowed(a.staticConfig, gvk) {
183-
return nil, isNotAllowedError(gvk)
184-
}
185-
return a.delegate.AuthenticationV1().TokenReviews(), nil
186-
}
187-
188-
func NewAccessControlClientset(cfg *rest.Config, staticConfig *config.StaticConfig) (*AccessControlClientset, error) {
189-
clientSet, err := kubernetes.NewForConfig(cfg)
190-
if err != nil {
191-
return nil, err
192-
}
193-
metricsClient, err := metricsv1beta1.NewForConfig(cfg)
194-
if err != nil {
195-
return nil, err
196-
}
197-
return &AccessControlClientset{
198-
cfg: cfg,
199-
delegate: clientSet,
200-
discoveryClient: clientSet.DiscoveryClient,
201-
metricsV1beta1: metricsClient,
202-
staticConfig: staticConfig,
203-
}, nil
195+
return a.AuthenticationV1().TokenReviews(), nil
204196
}

pkg/kubernetes/kubernetes_derived_test.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,7 @@ users:
122122
s.Run("derived manager has initialized clients", func() {
123123
// Verify that the derived manager has proper clients initialized
124124
s.NotNilf(derived.manager.accessControlClientSet, "expected accessControlClientSet to be initialized")
125-
s.Equalf(testStaticConfig, derived.manager.accessControlClientSet.staticConfig, "staticConfig not properly wired to derived manager")
126-
s.NotNilf(derived.manager.discoveryClient, "expected discoveryClient to be initialized")
127-
s.NotNilf(derived.manager.restMapper, "expected accessControlRESTMapper to be initialized")
128125
//s.Equalf(testStaticConfig, derived.manager.re.staticConfig, "staticConfig not properly wired to derived manager")
129-
s.NotNilf(derived.manager.dynamicClient, "expected dynamicClient to be initialized")
130126
})
131127
})
132128
})

0 commit comments

Comments
 (0)