Skip to content

Commit 54d564a

Browse files
committed
refactor(kubernetes): force usage of Derived kubernetes
Prevents consumers of the kubernetes package the usage of public methods on a non-derived config instance.
1 parent b07cd04 commit 54d564a

File tree

11 files changed

+101
-59
lines changed

11 files changed

+101
-59
lines changed

pkg/kubernetes/configuration.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ var InClusterConfig = func() (*rest.Config, error) {
2121
}
2222

2323
// resolveKubernetesConfigurations resolves the required kubernetes configurations and sets them in the Kubernetes struct
24-
func resolveKubernetesConfigurations(kubernetes *Kubernetes) error {
24+
func resolveKubernetesConfigurations(kubernetes *kubernetes) error {
2525
// Always set clientCmdConfig
2626
pathOptions := clientcmd.NewDefaultPathOptions()
2727
if kubernetes.Kubeconfig != "" {
@@ -45,39 +45,39 @@ func resolveKubernetesConfigurations(kubernetes *Kubernetes) error {
4545
return err
4646
}
4747

48-
func (k *Kubernetes) IsInCluster() bool {
48+
func (k *kubernetes) IsInCluster() bool {
4949
if k.Kubeconfig != "" {
5050
return false
5151
}
5252
cfg, err := InClusterConfig()
5353
return err == nil && cfg != nil
5454
}
5555

56-
func (k *Kubernetes) configuredNamespace() string {
56+
func (k *kubernetes) configuredNamespace() string {
5757
if ns, _, nsErr := k.clientCmdConfig.Namespace(); nsErr == nil {
5858
return ns
5959
}
6060
return ""
6161
}
6262

63-
func (k *Kubernetes) NamespaceOrDefault(namespace string) string {
63+
func (k *kubernetes) NamespaceOrDefault(namespace string) string {
6464
if namespace == "" {
6565
return k.configuredNamespace()
6666
}
6767
return namespace
6868
}
6969

7070
// ToRESTConfig returns the rest.Config object (genericclioptions.RESTClientGetter)
71-
func (k *Kubernetes) ToRESTConfig() (*rest.Config, error) {
71+
func (k *kubernetes) ToRESTConfig() (*rest.Config, error) {
7272
return k.cfg, nil
7373
}
7474

7575
// ToRawKubeConfigLoader returns the clientcmd.ClientConfig object (genericclioptions.RESTClientGetter)
76-
func (k *Kubernetes) ToRawKubeConfigLoader() clientcmd.ClientConfig {
76+
func (k *kubernetes) ToRawKubeConfigLoader() clientcmd.ClientConfig {
7777
return k.clientCmdConfig
7878
}
7979

80-
func (k *Kubernetes) ConfigurationView(minify bool) (runtime.Object, error) {
80+
func (k *kubernetes) ConfigurationView(minify bool) (runtime.Object, error) {
8181
var cfg clientcmdapi.Config
8282
var err error
8383
if k.IsInCluster() {

pkg/kubernetes/configuration_test.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212

1313
func TestKubernetes_IsInCluster(t *testing.T) {
1414
t.Run("with explicit kubeconfig", func(t *testing.T) {
15-
k := Kubernetes{
15+
k := kubernetes{
1616
Kubeconfig: "kubeconfig",
1717
}
1818
if k.IsInCluster() {
@@ -27,7 +27,7 @@ func TestKubernetes_IsInCluster(t *testing.T) {
2727
defer func() {
2828
InClusterConfig = originalFunction
2929
}()
30-
k := Kubernetes{
30+
k := kubernetes{
3131
Kubeconfig: "",
3232
}
3333
if !k.IsInCluster() {
@@ -42,7 +42,7 @@ func TestKubernetes_IsInCluster(t *testing.T) {
4242
defer func() {
4343
InClusterConfig = originalFunction
4444
}()
45-
k := Kubernetes{
45+
k := kubernetes{
4646
Kubeconfig: "",
4747
}
4848
if k.IsInCluster() {
@@ -57,7 +57,7 @@ func TestKubernetes_IsInCluster(t *testing.T) {
5757
defer func() {
5858
InClusterConfig = originalFunction
5959
}()
60-
k := Kubernetes{
60+
k := kubernetes{
6161
Kubeconfig: "",
6262
}
6363
if k.IsInCluster() {
@@ -72,7 +72,7 @@ func TestKubernetes_ResolveKubernetesConfigurations_Explicit(t *testing.T) {
7272
t.Skip("Skipping test on non-linux platforms")
7373
}
7474
tempDir := t.TempDir()
75-
k := Kubernetes{Kubeconfig: path.Join(tempDir, "config")}
75+
k := kubernetes{Kubeconfig: path.Join(tempDir, "config")}
7676
err := resolveKubernetesConfigurations(&k)
7777
if err == nil {
7878
t.Errorf("expected error, got nil")
@@ -90,7 +90,7 @@ func TestKubernetes_ResolveKubernetesConfigurations_Explicit(t *testing.T) {
9090
if err := os.WriteFile(kubeconfigPath, []byte(""), 0644); err != nil {
9191
t.Fatalf("failed to create kubeconfig file: %v", err)
9292
}
93-
k := Kubernetes{Kubeconfig: kubeconfigPath}
93+
k := kubernetes{Kubeconfig: kubeconfigPath}
9494
err := resolveKubernetesConfigurations(&k)
9595
if err == nil {
9696
t.Errorf("expected error, got nil")
@@ -123,7 +123,7 @@ users:
123123
if err := os.WriteFile(kubeconfigPath, []byte(kubeconfigContent), 0644); err != nil {
124124
t.Fatalf("failed to create kubeconfig file: %v", err)
125125
}
126-
k := Kubernetes{Kubeconfig: kubeconfigPath}
126+
k := kubernetes{Kubeconfig: kubeconfigPath}
127127
err := resolveKubernetesConfigurations(&k)
128128
if err != nil {
129129
t.Fatalf("expected no error, got %v", err)

pkg/kubernetes/events.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"strings"
1010
)
1111

12-
func (k *Kubernetes) EventsList(ctx context.Context, namespace string) ([]map[string]any, error) {
12+
func (k *kubernetes) EventsList(ctx context.Context, namespace string) ([]map[string]any, error) {
1313
var eventMap []map[string]any
1414
raw, err := k.ResourcesList(ctx, &schema.GroupVersionKind{
1515
Group: "", Version: "v1", Kind: "Event",

pkg/kubernetes/kubernetes.go

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,20 @@ import (
66
"github.com/manusa/kubernetes-mcp-server/pkg/helm"
77
v1 "k8s.io/api/core/v1"
88
"k8s.io/apimachinery/pkg/api/meta"
9+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
910
"k8s.io/apimachinery/pkg/runtime"
11+
"k8s.io/apimachinery/pkg/runtime/schema"
1012
"k8s.io/client-go/discovery"
1113
"k8s.io/client-go/discovery/cached/memory"
1214
"k8s.io/client-go/dynamic"
13-
"k8s.io/client-go/kubernetes"
15+
clientgokubernetes "k8s.io/client-go/kubernetes"
1416
_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
1517
"k8s.io/client-go/rest"
1618
"k8s.io/client-go/restmapper"
1719
"k8s.io/client-go/tools/clientcmd"
1820
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
1921
"k8s.io/klog/v2"
22+
"k8s.io/metrics/pkg/apis/metrics"
2023
"strings"
2124
)
2225

@@ -26,23 +29,51 @@ const (
2629

2730
type CloseWatchKubeConfig func() error
2831

29-
type Kubernetes struct {
32+
type Kubernetes interface {
33+
WatchKubeConfig(onKubeConfigChange func() error)
34+
Close()
35+
Derived(ctx context.Context) DerivedKubernetes
36+
ConfigurationView(minify bool) (runtime.Object, error)
37+
IsOpenShift(ctx context.Context) bool
38+
}
39+
40+
type DerivedKubernetes interface {
41+
IsOpenShift(ctx context.Context) bool
42+
CacheInvalidate()
43+
NewHelm() *helm.Helm
44+
EventsList(ctx context.Context, namespace string) ([]map[string]any, error)
45+
NamespacesList(ctx context.Context, options ResourceListOptions) (runtime.Unstructured, error)
46+
PodsListInAllNamespaces(ctx context.Context, options ResourceListOptions) (runtime.Unstructured, error)
47+
PodsListInNamespace(ctx context.Context, namespace string, options ResourceListOptions) (runtime.Unstructured, error)
48+
PodsGet(ctx context.Context, namespace, name string) (*unstructured.Unstructured, error)
49+
PodsDelete(ctx context.Context, namespace, name string) (string, error)
50+
PodsLog(ctx context.Context, namespace, name, container string) (string, error)
51+
PodsRun(ctx context.Context, namespace, name, image string, port int32) ([]*unstructured.Unstructured, error)
52+
PodsTop(ctx context.Context, options PodsTopOptions) (*metrics.PodMetricsList, error)
53+
PodsExec(ctx context.Context, namespace, name, container string, command []string) (string, error)
54+
ProjectsList(ctx context.Context, options ResourceListOptions) (runtime.Unstructured, error)
55+
ResourcesList(ctx context.Context, gvk *schema.GroupVersionKind, namespace string, options ResourceListOptions) (runtime.Unstructured, error)
56+
ResourcesGet(ctx context.Context, gvk *schema.GroupVersionKind, namespace, name string) (*unstructured.Unstructured, error)
57+
ResourcesCreateOrUpdate(ctx context.Context, resource string) ([]*unstructured.Unstructured, error)
58+
ResourcesDelete(ctx context.Context, gvk *schema.GroupVersionKind, namespace, name string) error
59+
}
60+
61+
type kubernetes struct {
3062
// Kubeconfig path override
3163
Kubeconfig string
3264
cfg *rest.Config
3365
clientCmdConfig clientcmd.ClientConfig
3466
CloseWatchKubeConfig CloseWatchKubeConfig
3567
scheme *runtime.Scheme
3668
parameterCodec runtime.ParameterCodec
37-
clientSet kubernetes.Interface
69+
clientSet clientgokubernetes.Interface
3870
discoveryClient discovery.CachedDiscoveryInterface
3971
deferredDiscoveryRESTMapper *restmapper.DeferredDiscoveryRESTMapper
4072
dynamicClient *dynamic.DynamicClient
41-
Helm *helm.Helm
4273
}
4374

44-
func NewKubernetes(kubeconfig string) (*Kubernetes, error) {
45-
k8s := &Kubernetes{
75+
func NewKubernetes(kubeconfig string) (Kubernetes, error) {
76+
k8s := &kubernetes{
4677
Kubeconfig: kubeconfig,
4778
}
4879
if err := resolveKubernetesConfigurations(k8s); err != nil {
@@ -53,7 +84,7 @@ func NewKubernetes(kubeconfig string) (*Kubernetes, error) {
5384
// return &impersonateRoundTripper{original}
5485
//})
5586
var err error
56-
k8s.clientSet, err = kubernetes.NewForConfig(k8s.cfg)
87+
k8s.clientSet, err = clientgokubernetes.NewForConfig(k8s.cfg)
5788
if err != nil {
5889
return nil, err
5990
}
@@ -68,11 +99,10 @@ func NewKubernetes(kubeconfig string) (*Kubernetes, error) {
6899
return nil, err
69100
}
70101
k8s.parameterCodec = runtime.NewParameterCodec(k8s.scheme)
71-
k8s.Helm = helm.NewHelm(k8s)
72102
return k8s, nil
73103
}
74104

75-
func (k *Kubernetes) WatchKubeConfig(onKubeConfigChange func() error) {
105+
func (k *kubernetes) WatchKubeConfig(onKubeConfigChange func() error) {
76106
if k.clientCmdConfig == nil {
77107
return
78108
}
@@ -108,21 +138,21 @@ func (k *Kubernetes) WatchKubeConfig(onKubeConfigChange func() error) {
108138
k.CloseWatchKubeConfig = watcher.Close
109139
}
110140

111-
func (k *Kubernetes) Close() {
141+
func (k *kubernetes) Close() {
112142
if k.CloseWatchKubeConfig != nil {
113143
_ = k.CloseWatchKubeConfig()
114144
}
115145
}
116146

117-
func (k *Kubernetes) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
147+
func (k *kubernetes) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
118148
return k.discoveryClient, nil
119149
}
120150

121-
func (k *Kubernetes) ToRESTMapper() (meta.RESTMapper, error) {
151+
func (k *kubernetes) ToRESTMapper() (meta.RESTMapper, error) {
122152
return k.deferredDiscoveryRESTMapper, nil
123153
}
124154

125-
func (k *Kubernetes) Derived(ctx context.Context) *Kubernetes {
155+
func (k *kubernetes) Derived(ctx context.Context) DerivedKubernetes {
126156
authorization, ok := ctx.Value(AuthorizationHeader).(string)
127157
if !ok || !strings.HasPrefix(authorization, "Bearer ") {
128158
return k
@@ -142,14 +172,14 @@ func (k *Kubernetes) Derived(ctx context.Context) *Kubernetes {
142172
return k
143173
}
144174
clientCmdApiConfig.AuthInfos = make(map[string]*clientcmdapi.AuthInfo)
145-
derived := &Kubernetes{
175+
derived := &kubernetes{
146176
Kubeconfig: k.Kubeconfig,
147177
clientCmdConfig: clientcmd.NewDefaultClientConfig(clientCmdApiConfig, nil),
148178
cfg: derivedCfg,
149179
scheme: k.scheme,
150180
parameterCodec: k.parameterCodec,
151181
}
152-
derived.clientSet, err = kubernetes.NewForConfig(derived.cfg)
182+
derived.clientSet, err = clientgokubernetes.NewForConfig(derived.cfg)
153183
if err != nil {
154184
return k
155185
}
@@ -159,6 +189,19 @@ func (k *Kubernetes) Derived(ctx context.Context) *Kubernetes {
159189
if err != nil {
160190
return k
161191
}
162-
derived.Helm = helm.NewHelm(derived)
163192
return derived
164193
}
194+
195+
func (k *kubernetes) CacheInvalidate() {
196+
if k.discoveryClient != nil {
197+
k.discoveryClient.Invalidate()
198+
}
199+
if k.deferredDiscoveryRESTMapper != nil {
200+
k.deferredDiscoveryRESTMapper.Reset()
201+
}
202+
}
203+
204+
func (k *kubernetes) NewHelm() *helm.Helm {
205+
// This is a derived Kubernetes, so it already has the Helm initialized
206+
return helm.NewHelm(k)
207+
}

pkg/kubernetes/namespaces.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import (
66
"k8s.io/apimachinery/pkg/runtime/schema"
77
)
88

9-
func (k *Kubernetes) NamespacesList(ctx context.Context, options ResourceListOptions) (runtime.Unstructured, error) {
9+
func (k *kubernetes) NamespacesList(ctx context.Context, options ResourceListOptions) (runtime.Unstructured, error) {
1010
return k.ResourcesList(ctx, &schema.GroupVersionKind{
1111
Group: "", Version: "v1", Kind: "Namespace",
1212
}, "", options)
1313
}
1414

15-
func (k *Kubernetes) ProjectsList(ctx context.Context, options ResourceListOptions) (runtime.Unstructured, error) {
15+
func (k *kubernetes) ProjectsList(ctx context.Context, options ResourceListOptions) (runtime.Unstructured, error) {
1616
return k.ResourcesList(ctx, &schema.GroupVersionKind{
1717
Group: "project.openshift.io", Version: "v1", Kind: "Project",
1818
}, "", options)

pkg/kubernetes/openshift.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"k8s.io/apimachinery/pkg/runtime/schema"
77
)
88

9-
func (k *Kubernetes) IsOpenShift(ctx context.Context) bool {
9+
func (k *kubernetes) IsOpenShift(ctx context.Context) bool {
1010
// This method should be fast and not block (it's called at startup)
1111
timeoutSeconds := int64(5)
1212
if _, err := k.dynamicClient.Resource(schema.GroupVersionResource{

pkg/kubernetes/pods.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,25 +29,25 @@ type PodsTopOptions struct {
2929
Name string
3030
}
3131

32-
func (k *Kubernetes) PodsListInAllNamespaces(ctx context.Context, options ResourceListOptions) (runtime.Unstructured, error) {
32+
func (k *kubernetes) PodsListInAllNamespaces(ctx context.Context, options ResourceListOptions) (runtime.Unstructured, error) {
3333
return k.ResourcesList(ctx, &schema.GroupVersionKind{
3434
Group: "", Version: "v1", Kind: "Pod",
3535
}, "", options)
3636
}
3737

38-
func (k *Kubernetes) PodsListInNamespace(ctx context.Context, namespace string, options ResourceListOptions) (runtime.Unstructured, error) {
38+
func (k *kubernetes) PodsListInNamespace(ctx context.Context, namespace string, options ResourceListOptions) (runtime.Unstructured, error) {
3939
return k.ResourcesList(ctx, &schema.GroupVersionKind{
4040
Group: "", Version: "v1", Kind: "Pod",
4141
}, namespace, options)
4242
}
4343

44-
func (k *Kubernetes) PodsGet(ctx context.Context, namespace, name string) (*unstructured.Unstructured, error) {
44+
func (k *kubernetes) PodsGet(ctx context.Context, namespace, name string) (*unstructured.Unstructured, error) {
4545
return k.ResourcesGet(ctx, &schema.GroupVersionKind{
4646
Group: "", Version: "v1", Kind: "Pod",
4747
}, k.NamespaceOrDefault(namespace), name)
4848
}
4949

50-
func (k *Kubernetes) PodsDelete(ctx context.Context, namespace, name string) (string, error) {
50+
func (k *kubernetes) PodsDelete(ctx context.Context, namespace, name string) (string, error) {
5151
namespace = k.NamespaceOrDefault(namespace)
5252
pod, err := k.clientSet.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{})
5353
if err != nil {
@@ -89,7 +89,7 @@ func (k *Kubernetes) PodsDelete(ctx context.Context, namespace, name string) (st
8989
k.clientSet.CoreV1().Pods(namespace).Delete(ctx, name, metav1.DeleteOptions{})
9090
}
9191

92-
func (k *Kubernetes) PodsLog(ctx context.Context, namespace, name, container string) (string, error) {
92+
func (k *kubernetes) PodsLog(ctx context.Context, namespace, name, container string) (string, error) {
9393
tailLines := int64(256)
9494
req := k.clientSet.CoreV1().Pods(k.NamespaceOrDefault(namespace)).GetLogs(name, &v1.PodLogOptions{
9595
TailLines: &tailLines,
@@ -106,7 +106,7 @@ func (k *Kubernetes) PodsLog(ctx context.Context, namespace, name, container str
106106
return string(rawData), nil
107107
}
108108

109-
func (k *Kubernetes) PodsRun(ctx context.Context, namespace, name, image string, port int32) ([]*unstructured.Unstructured, error) {
109+
func (k *kubernetes) PodsRun(ctx context.Context, namespace, name, image string, port int32) ([]*unstructured.Unstructured, error) {
110110
if name == "" {
111111
name = version.BinaryName + "-run-" + rand.String(5)
112112
}
@@ -186,7 +186,7 @@ func (k *Kubernetes) PodsRun(ctx context.Context, namespace, name, image string,
186186
return k.resourcesCreateOrUpdate(ctx, toCreate)
187187
}
188188

189-
func (k *Kubernetes) PodsTop(ctx context.Context, options PodsTopOptions) (*metrics.PodMetricsList, error) {
189+
func (k *kubernetes) PodsTop(ctx context.Context, options PodsTopOptions) (*metrics.PodMetricsList, error) {
190190
// TODO, maybe move to mcp Tools setup and omit in case metrics aren't available in the target cluster
191191
if !k.supportsGroupVersion(metrics.GroupName + "/" + metricsv1beta1api.SchemeGroupVersion.Version) {
192192
return nil, errors.New("metrics API is not available")
@@ -218,7 +218,7 @@ func (k *Kubernetes) PodsTop(ctx context.Context, options PodsTopOptions) (*metr
218218
return convertedMetrics, metricsv1beta1api.Convert_v1beta1_PodMetricsList_To_metrics_PodMetricsList(versionedMetrics, convertedMetrics, nil)
219219
}
220220

221-
func (k *Kubernetes) PodsExec(ctx context.Context, namespace, name, container string, command []string) (string, error) {
221+
func (k *kubernetes) PodsExec(ctx context.Context, namespace, name, container string, command []string) (string, error) {
222222
namespace = k.NamespaceOrDefault(namespace)
223223
pod, err := k.clientSet.CoreV1().Pods(namespace).Get(ctx, name, metav1.GetOptions{})
224224
if err != nil {
@@ -257,7 +257,7 @@ func (k *Kubernetes) PodsExec(ctx context.Context, namespace, name, container st
257257
return "", nil
258258
}
259259

260-
func (k *Kubernetes) createExecutor(namespace, name string, podExecOptions *v1.PodExecOptions) (remotecommand.Executor, error) {
260+
func (k *kubernetes) createExecutor(namespace, name string, podExecOptions *v1.PodExecOptions) (remotecommand.Executor, error) {
261261
// Compute URL
262262
// https://github.com/kubernetes/kubectl/blob/5366de04e168bcbc11f5e340d131a9ca8b7d0df4/pkg/cmd/exec/exec.go#L382-L397
263263
req := k.clientSet.CoreV1().RESTClient().

0 commit comments

Comments
 (0)