|  | 
|  | 1 | +package kubernetes | 
|  | 2 | + | 
|  | 3 | +import ( | 
|  | 4 | +	"context" | 
|  | 5 | +	"fmt" | 
|  | 6 | + | 
|  | 7 | +	authorizationv1api "k8s.io/api/authorization/v1" | 
|  | 8 | +	v1 "k8s.io/api/core/v1" | 
|  | 9 | +	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | 
|  | 10 | +	"k8s.io/apimachinery/pkg/runtime/schema" | 
|  | 11 | +	"k8s.io/apimachinery/pkg/util/httpstream" | 
|  | 12 | +	"k8s.io/client-go/discovery" | 
|  | 13 | +	"k8s.io/client-go/kubernetes" | 
|  | 14 | +	authorizationv1 "k8s.io/client-go/kubernetes/typed/authorization/v1" | 
|  | 15 | +	corev1 "k8s.io/client-go/kubernetes/typed/core/v1" | 
|  | 16 | +	"k8s.io/client-go/rest" | 
|  | 17 | +	"k8s.io/client-go/tools/remotecommand" | 
|  | 18 | +	"k8s.io/metrics/pkg/apis/metrics" | 
|  | 19 | +	metricsv1beta1api "k8s.io/metrics/pkg/apis/metrics/v1beta1" | 
|  | 20 | +	metricsv1beta1 "k8s.io/metrics/pkg/client/clientset/versioned/typed/metrics/v1beta1" | 
|  | 21 | + | 
|  | 22 | +	"github.com/manusa/kubernetes-mcp-server/pkg/config" | 
|  | 23 | +) | 
|  | 24 | + | 
|  | 25 | +// AccessControlClientset is a limited clientset delegating interface to the standard kubernetes.Clientset | 
|  | 26 | +// Only a limited set of functions are implemented with a single point of access to the kubernetes API where | 
|  | 27 | +// apiVersion and kinds are checked for allowed access | 
|  | 28 | +type AccessControlClientset struct { | 
|  | 29 | +	cfg             *rest.Config | 
|  | 30 | +	delegate        kubernetes.Interface | 
|  | 31 | +	discoveryClient discovery.DiscoveryInterface | 
|  | 32 | +	metricsV1beta1  *metricsv1beta1.MetricsV1beta1Client | 
|  | 33 | +	staticConfig    *config.StaticConfig // TODO: maybe just store the denied resource slice | 
|  | 34 | +} | 
|  | 35 | + | 
|  | 36 | +func (a *AccessControlClientset) DiscoveryClient() discovery.DiscoveryInterface { | 
|  | 37 | +	return a.discoveryClient | 
|  | 38 | +} | 
|  | 39 | + | 
|  | 40 | +func (a *AccessControlClientset) Pods(namespace string) (corev1.PodInterface, error) { | 
|  | 41 | +	gvk := &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"} | 
|  | 42 | +	if !isAllowed(a.staticConfig, gvk) { | 
|  | 43 | +		return nil, isNotAllowedError(gvk) | 
|  | 44 | +	} | 
|  | 45 | +	return a.delegate.CoreV1().Pods(namespace), nil | 
|  | 46 | +} | 
|  | 47 | + | 
|  | 48 | +func (a *AccessControlClientset) PodsExec(namespace, name string, podExecOptions *v1.PodExecOptions) (remotecommand.Executor, error) { | 
|  | 49 | +	gvk := &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"} | 
|  | 50 | +	if !isAllowed(a.staticConfig, gvk) { | 
|  | 51 | +		return nil, isNotAllowedError(gvk) | 
|  | 52 | +	} | 
|  | 53 | +	// Compute URL | 
|  | 54 | +	// https://github.com/kubernetes/kubectl/blob/5366de04e168bcbc11f5e340d131a9ca8b7d0df4/pkg/cmd/exec/exec.go#L382-L397 | 
|  | 55 | +	execRequest := a.delegate.CoreV1().RESTClient(). | 
|  | 56 | +		Post(). | 
|  | 57 | +		Resource("pods"). | 
|  | 58 | +		Namespace(namespace). | 
|  | 59 | +		Name(name). | 
|  | 60 | +		SubResource("exec") | 
|  | 61 | +	execRequest.VersionedParams(podExecOptions, ParameterCodec) | 
|  | 62 | +	spdyExec, err := remotecommand.NewSPDYExecutor(a.cfg, "POST", execRequest.URL()) | 
|  | 63 | +	if err != nil { | 
|  | 64 | +		return nil, err | 
|  | 65 | +	} | 
|  | 66 | +	webSocketExec, err := remotecommand.NewWebSocketExecutor(a.cfg, "GET", execRequest.URL().String()) | 
|  | 67 | +	if err != nil { | 
|  | 68 | +		return nil, err | 
|  | 69 | +	} | 
|  | 70 | +	return remotecommand.NewFallbackExecutor(webSocketExec, spdyExec, func(err error) bool { | 
|  | 71 | +		return httpstream.IsUpgradeFailure(err) || httpstream.IsHTTPSProxyError(err) | 
|  | 72 | +	}) | 
|  | 73 | +} | 
|  | 74 | + | 
|  | 75 | +func (a *AccessControlClientset) PodsMetricses(ctx context.Context, namespace, name string, listOptions metav1.ListOptions) (*metrics.PodMetricsList, error) { | 
|  | 76 | +	gvk := &schema.GroupVersionKind{Group: metrics.GroupName, Version: metricsv1beta1api.SchemeGroupVersion.Version, Kind: "PodMetrics"} | 
|  | 77 | +	if !isAllowed(a.staticConfig, gvk) { | 
|  | 78 | +		return nil, isNotAllowedError(gvk) | 
|  | 79 | +	} | 
|  | 80 | +	versionedMetrics := &metricsv1beta1api.PodMetricsList{} | 
|  | 81 | +	var err error | 
|  | 82 | +	if name != "" { | 
|  | 83 | +		m, err := a.metricsV1beta1.PodMetricses(namespace).Get(ctx, name, metav1.GetOptions{}) | 
|  | 84 | +		if err != nil { | 
|  | 85 | +			return nil, fmt.Errorf("failed to get metrics for pod %s/%s: %w", namespace, name, err) | 
|  | 86 | +		} | 
|  | 87 | +		versionedMetrics.Items = []metricsv1beta1api.PodMetrics{*m} | 
|  | 88 | +	} else { | 
|  | 89 | +		versionedMetrics, err = a.metricsV1beta1.PodMetricses(namespace).List(ctx, listOptions) | 
|  | 90 | +		if err != nil { | 
|  | 91 | +			return nil, fmt.Errorf("failed to list pod metrics in namespace %s: %w", namespace, err) | 
|  | 92 | +		} | 
|  | 93 | +	} | 
|  | 94 | +	convertedMetrics := &metrics.PodMetricsList{} | 
|  | 95 | +	return convertedMetrics, metricsv1beta1api.Convert_v1beta1_PodMetricsList_To_metrics_PodMetricsList(versionedMetrics, convertedMetrics, nil) | 
|  | 96 | +} | 
|  | 97 | + | 
|  | 98 | +func (a *AccessControlClientset) Services(namespace string) (corev1.ServiceInterface, error) { | 
|  | 99 | +	gvk := &schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Service"} | 
|  | 100 | +	if !isAllowed(a.staticConfig, gvk) { | 
|  | 101 | +		return nil, isNotAllowedError(gvk) | 
|  | 102 | +	} | 
|  | 103 | +	return a.delegate.CoreV1().Services(namespace), nil | 
|  | 104 | +} | 
|  | 105 | + | 
|  | 106 | +func (a *AccessControlClientset) SelfSubjectAccessReviews() (authorizationv1.SelfSubjectAccessReviewInterface, error) { | 
|  | 107 | +	gvk := &schema.GroupVersionKind{Group: authorizationv1api.GroupName, Version: authorizationv1api.SchemeGroupVersion.Version, Kind: "SelfSubjectAccessReview"} | 
|  | 108 | +	if !isAllowed(a.staticConfig, gvk) { | 
|  | 109 | +		return nil, isNotAllowedError(gvk) | 
|  | 110 | +	} | 
|  | 111 | +	return a.delegate.AuthorizationV1().SelfSubjectAccessReviews(), nil | 
|  | 112 | +} | 
|  | 113 | + | 
|  | 114 | +func NewAccessControlClientset(cfg *rest.Config, staticConfig *config.StaticConfig) (*AccessControlClientset, error) { | 
|  | 115 | +	clientSet, err := kubernetes.NewForConfig(cfg) | 
|  | 116 | +	if err != nil { | 
|  | 117 | +		return nil, err | 
|  | 118 | +	} | 
|  | 119 | +	metricsClient, err := metricsv1beta1.NewForConfig(cfg) | 
|  | 120 | +	if err != nil { | 
|  | 121 | +		return nil, err | 
|  | 122 | +	} | 
|  | 123 | +	return &AccessControlClientset{ | 
|  | 124 | +		cfg:             cfg, | 
|  | 125 | +		delegate:        clientSet, | 
|  | 126 | +		discoveryClient: clientSet.DiscoveryClient, | 
|  | 127 | +		metricsV1beta1:  metricsClient, | 
|  | 128 | +		staticConfig:    staticConfig, | 
|  | 129 | +	}, nil | 
|  | 130 | +} | 
0 commit comments