|
1 | 1 | package kubernetes |
2 | 2 |
|
3 | 3 | import ( |
4 | | - "context" |
5 | 4 | "fmt" |
| 5 | + "net/http" |
6 | 6 |
|
7 | | - authenticationv1api "k8s.io/api/authentication/v1" |
8 | | - authorizationv1api "k8s.io/api/authorization/v1" |
9 | | - v1 "k8s.io/api/core/v1" |
10 | | - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
11 | | - "k8s.io/apimachinery/pkg/runtime/schema" |
12 | | - "k8s.io/apimachinery/pkg/util/httpstream" |
| 7 | + "github.com/containers/kubernetes-mcp-server/pkg/config" |
| 8 | + "k8s.io/apimachinery/pkg/api/meta" |
13 | 9 | "k8s.io/client-go/discovery" |
| 10 | + "k8s.io/client-go/discovery/cached/memory" |
| 11 | + "k8s.io/client-go/dynamic" |
14 | 12 | "k8s.io/client-go/kubernetes" |
15 | 13 | authenticationv1 "k8s.io/client-go/kubernetes/typed/authentication/v1" |
16 | 14 | authorizationv1 "k8s.io/client-go/kubernetes/typed/authorization/v1" |
17 | 15 | corev1 "k8s.io/client-go/kubernetes/typed/core/v1" |
18 | 16 | "k8s.io/client-go/rest" |
19 | | - "k8s.io/client-go/tools/remotecommand" |
20 | | - "k8s.io/metrics/pkg/apis/metrics" |
21 | | - metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1" |
| 17 | + "k8s.io/client-go/restmapper" |
22 | 18 | metricsv1beta1 "k8s.io/metrics/pkg/client/clientset/versioned/typed/metrics/v1beta1" |
23 | | - |
24 | | - "github.com/containers/kubernetes-mcp-server/pkg/config" |
25 | 19 | ) |
26 | 20 |
|
27 | 21 | // AccessControlClientset is a limited clientset delegating interface to the standard kubernetes.Clientset |
28 | 22 | // Only a limited set of functions are implemented with a single point of access to the kubernetes API where |
29 | 23 | // apiVersion and kinds are checked for allowed access |
30 | 24 | type AccessControlClientset struct { |
31 | | - cfg *rest.Config |
32 | | - delegate kubernetes.Interface |
33 | | - discoveryClient discovery.DiscoveryInterface |
| 25 | + cfg *rest.Config |
| 26 | + kubernetes.Interface |
| 27 | + restMapper meta.ResettableRESTMapper |
| 28 | + discoveryClient discovery.CachedDiscoveryInterface |
| 29 | + dynamicClient dynamic.Interface |
34 | 30 | metricsV1beta1 *metricsv1beta1.MetricsV1beta1Client |
35 | | - staticConfig *config.StaticConfig // TODO: maybe just store the denied resource slice |
36 | | -} |
37 | | - |
38 | | -func (a *AccessControlClientset) DiscoveryClient() discovery.DiscoveryInterface { |
39 | | - return a.discoveryClient |
40 | 31 | } |
41 | 32 |
|
42 | | -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) |
| 33 | +func NewAccessControlClientset(staticConfig *config.StaticConfig, restConfig *rest.Config) (*AccessControlClientset, error) { |
| 34 | + rest.CopyConfig(restConfig) |
| 35 | + acc := &AccessControlClientset{ |
| 36 | + cfg: rest.CopyConfig(restConfig), |
46 | 37 | } |
47 | | - return a.delegate.CoreV1().Nodes(), nil |
48 | | -} |
49 | | - |
50 | | -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) |
| 38 | + if acc.cfg.UserAgent == "" { |
| 39 | + acc.cfg.UserAgent = rest.DefaultKubernetesUserAgent() |
54 | 40 | } |
55 | | - |
56 | | - if _, err := a.delegate.CoreV1().Nodes().Get(ctx, name, metav1.GetOptions{}); err != nil { |
57 | | - return nil, fmt.Errorf("failed to get node %s: %w", name, err) |
| 41 | + acc.cfg.Wrap(func(original http.RoundTripper) http.RoundTripper { |
| 42 | + return &AccessControlRoundTripper{ |
| 43 | + delegate: original, |
| 44 | + staticConfig: staticConfig, |
| 45 | + restMapper: acc.restMapper, |
| 46 | + } |
| 47 | + }) |
| 48 | + discoveryClient, err := discovery.NewDiscoveryClientForConfig(acc.cfg) |
| 49 | + if err != nil { |
| 50 | + return nil, fmt.Errorf("failed to create discovery client: %v", err) |
58 | 51 | } |
59 | | - |
60 | | - url := []string{"api", "v1", "nodes", name, "proxy", "logs"} |
61 | | - return a.delegate.CoreV1().RESTClient(). |
62 | | - Get(). |
63 | | - AbsPath(url...), nil |
64 | | -} |
65 | | - |
66 | | -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) |
| 52 | + acc.discoveryClient = memory.NewMemCacheClient(discoveryClient) |
| 53 | + acc.restMapper = restmapper.NewDeferredDiscoveryRESTMapper(acc.discoveryClient) |
| 54 | + acc.Interface, err = kubernetes.NewForConfig(acc.cfg) |
| 55 | + if err != nil { |
| 56 | + return nil, err |
70 | 57 | } |
71 | | - versionedMetrics := &metricsv1beta1api.NodeMetricsList{} |
72 | | - var err error |
73 | | - if name != "" { |
74 | | - m, err := a.metricsV1beta1.NodeMetricses().Get(ctx, name, metav1.GetOptions{}) |
75 | | - if err != nil { |
76 | | - return nil, fmt.Errorf("failed to get metrics for node %s: %w", name, err) |
77 | | - } |
78 | | - versionedMetrics.Items = []metricsv1beta1api.NodeMetrics{*m} |
79 | | - } else { |
80 | | - versionedMetrics, err = a.metricsV1beta1.NodeMetricses().List(ctx, listOptions) |
81 | | - if err != nil { |
82 | | - return nil, fmt.Errorf("failed to list node metrics: %w", err) |
83 | | - } |
| 58 | + acc.dynamicClient, err = dynamic.NewForConfig(acc.cfg) |
| 59 | + if err != nil { |
| 60 | + return nil, err |
84 | 61 | } |
85 | | - convertedMetrics := &metrics.NodeMetricsList{} |
86 | | - return convertedMetrics, metricsv1beta1api.Convert_v1beta1_NodeMetricsList_To_metrics_NodeMetricsList(versionedMetrics, convertedMetrics, nil) |
| 62 | + acc.metricsV1beta1, err = metricsv1beta1.NewForConfig(acc.cfg) |
| 63 | + if err != nil { |
| 64 | + return nil, err |
| 65 | + } |
| 66 | + return acc, nil |
87 | 67 | } |
88 | 68 |
|
89 | | -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 | | - } |
| 69 | +func (a *AccessControlClientset) RESTMapper() meta.ResettableRESTMapper { |
| 70 | + return a.restMapper |
| 71 | +} |
94 | 72 |
|
95 | | - if _, err := a.delegate.CoreV1().Nodes().Get(ctx, name, metav1.GetOptions{}); err != nil { |
96 | | - return nil, fmt.Errorf("failed to get node %s: %w", name, err) |
97 | | - } |
| 73 | +func (a *AccessControlClientset) DiscoveryClient() discovery.CachedDiscoveryInterface { |
| 74 | + return a.discoveryClient |
| 75 | +} |
98 | 76 |
|
99 | | - url := []string{"api", "v1", "nodes", name, "proxy", "stats", "summary"} |
100 | | - return a.delegate.CoreV1().RESTClient(). |
101 | | - Get(). |
102 | | - AbsPath(url...), nil |
| 77 | +func (a *AccessControlClientset) DynamicClient() dynamic.Interface { |
| 78 | + return a.dynamicClient |
103 | 79 | } |
104 | 80 |
|
105 | | -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 |
| 81 | +func (a *AccessControlClientset) MetricsV1beta1Client() *metricsv1beta1.MetricsV1beta1Client { |
| 82 | + return a.metricsV1beta1 |
111 | 83 | } |
112 | 84 |
|
113 | | -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 | | - } |
118 | | - // Compute URL |
119 | | - // https://github.com/kubernetes/kubectl/blob/5366de04e168bcbc11f5e340d131a9ca8b7d0df4/pkg/cmd/exec/exec.go#L382-L397 |
120 | | - execRequest := a.delegate.CoreV1().RESTClient(). |
121 | | - Post(). |
122 | | - Resource("pods"). |
123 | | - Namespace(namespace). |
124 | | - Name(name). |
125 | | - SubResource("exec") |
126 | | - execRequest.VersionedParams(podExecOptions, ParameterCodec) |
127 | | - spdyExec, err := remotecommand.NewSPDYExecutor(a.cfg, "POST", execRequest.URL()) |
128 | | - if err != nil { |
129 | | - return nil, err |
130 | | - } |
131 | | - webSocketExec, err := remotecommand.NewWebSocketExecutor(a.cfg, "GET", execRequest.URL().String()) |
132 | | - if err != nil { |
133 | | - return nil, err |
134 | | - } |
135 | | - return remotecommand.NewFallbackExecutor(webSocketExec, spdyExec, func(err error) bool { |
136 | | - return httpstream.IsUpgradeFailure(err) || httpstream.IsHTTPSProxyError(err) |
137 | | - }) |
| 85 | +// Nodes returns NodeInterface |
| 86 | +// Deprecated: use CoreV1().Nodes() directly |
| 87 | +func (a *AccessControlClientset) Nodes() (corev1.NodeInterface, error) { |
| 88 | + return a.CoreV1().Nodes(), nil |
138 | 89 | } |
139 | 90 |
|
140 | | -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 | | - } |
145 | | - versionedMetrics := &metricsv1beta1api.PodMetricsList{} |
146 | | - var err error |
147 | | - if name != "" { |
148 | | - m, err := a.metricsV1beta1.PodMetricses(namespace).Get(ctx, name, metav1.GetOptions{}) |
149 | | - if err != nil { |
150 | | - return nil, fmt.Errorf("failed to get metrics for pod %s/%s: %w", namespace, name, err) |
151 | | - } |
152 | | - versionedMetrics.Items = []metricsv1beta1api.PodMetrics{*m} |
153 | | - } else { |
154 | | - versionedMetrics, err = a.metricsV1beta1.PodMetricses(namespace).List(ctx, listOptions) |
155 | | - if err != nil { |
156 | | - return nil, fmt.Errorf("failed to list pod metrics in namespace %s: %w", namespace, err) |
157 | | - } |
158 | | - } |
159 | | - convertedMetrics := &metrics.PodMetricsList{} |
160 | | - return convertedMetrics, metricsv1beta1api.Convert_v1beta1_PodMetricsList_To_metrics_PodMetricsList(versionedMetrics, convertedMetrics, nil) |
| 91 | +// Pods returns PodInterface |
| 92 | +// Deprecated: use CoreV1().Pods(namespace) directly |
| 93 | +func (a *AccessControlClientset) Pods(namespace string) (corev1.PodInterface, error) { |
| 94 | + return a.CoreV1().Pods(namespace), nil |
161 | 95 | } |
162 | 96 |
|
| 97 | +// Services returns ServiceInterface |
| 98 | +// Deprecated: use CoreV1().Services(namespace) directly |
163 | 99 | 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 |
| 100 | + return a.CoreV1().Services(namespace), nil |
169 | 101 | } |
170 | 102 |
|
| 103 | +// SelfSubjectAccessReviews returns SelfSubjectAccessReviewInterface |
| 104 | +// Deprecated: use AuthorizationV1().SelfSubjectAccessReviews() directly |
171 | 105 | 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 |
| 106 | + return a.AuthorizationV1().SelfSubjectAccessReviews(), nil |
177 | 107 | } |
178 | 108 |
|
179 | 109 | // TokenReview returns TokenReviewInterface |
| 110 | +// Deprecated: use AuthenticationV1().TokenReviews() directly |
180 | 111 | 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 |
| 112 | + return a.AuthenticationV1().TokenReviews(), nil |
204 | 113 | } |
0 commit comments