Skip to main content
Glama

kubernetes-mcp-server

resources.go7.75 kB
package kubernetes import ( "context" "fmt" "k8s.io/apimachinery/pkg/runtime" "regexp" "strings" "github.com/containers/kubernetes-mcp-server/pkg/version" authv1 "k8s.io/api/authorization/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/yaml" ) const ( AppKubernetesComponent = "app.kubernetes.io/component" AppKubernetesManagedBy = "app.kubernetes.io/managed-by" AppKubernetesName = "app.kubernetes.io/name" AppKubernetesPartOf = "app.kubernetes.io/part-of" ) type ResourceListOptions struct { metav1.ListOptions AsTable bool } func (k *Kubernetes) ResourcesList(ctx context.Context, gvk *schema.GroupVersionKind, namespace string, options ResourceListOptions) (runtime.Unstructured, error) { gvr, err := k.resourceFor(gvk) if err != nil { return nil, err } // Check if operation is allowed for all namespaces (applicable for namespaced resources) isNamespaced, _ := k.isNamespaced(gvk) if isNamespaced && !k.canIUse(ctx, gvr, namespace, "list") && namespace == "" { namespace = k.manager.configuredNamespace() } if options.AsTable { return k.resourcesListAsTable(ctx, gvk, gvr, namespace, options) } return k.manager.dynamicClient.Resource(*gvr).Namespace(namespace).List(ctx, options.ListOptions) } func (k *Kubernetes) ResourcesGet(ctx context.Context, gvk *schema.GroupVersionKind, namespace, name string) (*unstructured.Unstructured, error) { gvr, err := k.resourceFor(gvk) if err != nil { return nil, err } // If it's a namespaced resource and namespace wasn't provided, try to use the default configured one if namespaced, nsErr := k.isNamespaced(gvk); nsErr == nil && namespaced { namespace = k.NamespaceOrDefault(namespace) } return k.manager.dynamicClient.Resource(*gvr).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) } func (k *Kubernetes) ResourcesCreateOrUpdate(ctx context.Context, resource string) ([]*unstructured.Unstructured, error) { separator := regexp.MustCompile(`\r?\n---\r?\n`) resources := separator.Split(resource, -1) var parsedResources []*unstructured.Unstructured for _, r := range resources { var obj unstructured.Unstructured if err := yaml.NewYAMLToJSONDecoder(strings.NewReader(r)).Decode(&obj); err != nil { return nil, err } parsedResources = append(parsedResources, &obj) } return k.resourcesCreateOrUpdate(ctx, parsedResources) } func (k *Kubernetes) ResourcesDelete(ctx context.Context, gvk *schema.GroupVersionKind, namespace, name string) error { gvr, err := k.resourceFor(gvk) if err != nil { return err } // If it's a namespaced resource and namespace wasn't provided, try to use the default configured one if namespaced, nsErr := k.isNamespaced(gvk); nsErr == nil && namespaced { namespace = k.NamespaceOrDefault(namespace) } return k.manager.dynamicClient.Resource(*gvr).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{}) } // resourcesListAsTable retrieves a list of resources in a table format. // It's almost identical to the dynamic.DynamicClient implementation, but it uses a specific Accept header to request the table format. // dynamic.DynamicClient does not provide a way to set the HTTP header (TODO: create an issue to request this feature) func (k *Kubernetes) resourcesListAsTable(ctx context.Context, gvk *schema.GroupVersionKind, gvr *schema.GroupVersionResource, namespace string, options ResourceListOptions) (runtime.Unstructured, error) { var url []string if len(gvr.Group) == 0 { url = append(url, "api") } else { url = append(url, "apis", gvr.Group) } url = append(url, gvr.Version) if len(namespace) > 0 { url = append(url, "namespaces", namespace) } url = append(url, gvr.Resource) var table metav1.Table err := k.manager.discoveryClient.RESTClient(). Get(). SetHeader("Accept", strings.Join([]string{ fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1.SchemeGroupVersion.Version, metav1.GroupName), fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName), "application/json", }, ",")). AbsPath(url...). SpecificallyVersionedParams(&options.ListOptions, ParameterCodec, schema.GroupVersion{Version: "v1"}). Do(ctx).Into(&table) if err != nil { return nil, err } // Add metav1.Table apiVersion and kind to the unstructured object (server may not return these fields) table.SetGroupVersionKind(metav1.SchemeGroupVersion.WithKind("Table")) // Add additional columns for fields that aren't returned by the server table.ColumnDefinitions = append([]metav1.TableColumnDefinition{ {Name: "apiVersion", Type: "string"}, {Name: "kind", Type: "string"}, }, table.ColumnDefinitions...) for i := range table.Rows { row := &table.Rows[i] row.Cells = append([]interface{}{ gvr.GroupVersion().String(), gvk.Kind, }, row.Cells...) } unstructuredObject, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&table) return &unstructured.Unstructured{Object: unstructuredObject}, err } func (k *Kubernetes) resourcesCreateOrUpdate(ctx context.Context, resources []*unstructured.Unstructured) ([]*unstructured.Unstructured, error) { for i, obj := range resources { gvk := obj.GroupVersionKind() gvr, rErr := k.resourceFor(&gvk) if rErr != nil { return nil, rErr } namespace := obj.GetNamespace() // If it's a namespaced resource and namespace wasn't provided, try to use the default configured one if namespaced, nsErr := k.isNamespaced(&gvk); nsErr == nil && namespaced { namespace = k.NamespaceOrDefault(namespace) } resources[i], rErr = k.manager.dynamicClient.Resource(*gvr).Namespace(namespace).Apply(ctx, obj.GetName(), obj, metav1.ApplyOptions{ FieldManager: version.BinaryName, }) if rErr != nil { return nil, rErr } // Clear the cache to ensure the next operation is performed on the latest exposed APIs (will change after the CRD creation) if gvk.Kind == "CustomResourceDefinition" { k.manager.accessControlRESTMapper.Reset() } } return resources, nil } func (k *Kubernetes) resourceFor(gvk *schema.GroupVersionKind) (*schema.GroupVersionResource, error) { m, err := k.manager.accessControlRESTMapper.RESTMapping(schema.GroupKind{Group: gvk.Group, Kind: gvk.Kind}, gvk.Version) if err != nil { return nil, err } return &m.Resource, nil } func (k *Kubernetes) isNamespaced(gvk *schema.GroupVersionKind) (bool, error) { apiResourceList, err := k.manager.discoveryClient.ServerResourcesForGroupVersion(gvk.GroupVersion().String()) if err != nil { return false, err } for _, apiResource := range apiResourceList.APIResources { if apiResource.Kind == gvk.Kind { return apiResource.Namespaced, nil } } return false, nil } func (k *Kubernetes) supportsGroupVersion(groupVersion string) bool { if _, err := k.manager.discoveryClient.ServerResourcesForGroupVersion(groupVersion); err != nil { return false } return true } func (k *Kubernetes) canIUse(ctx context.Context, gvr *schema.GroupVersionResource, namespace, verb string) bool { accessReviews, err := k.manager.accessControlClientSet.SelfSubjectAccessReviews() if err != nil { return false } response, err := accessReviews.Create(ctx, &authv1.SelfSubjectAccessReview{ Spec: authv1.SelfSubjectAccessReviewSpec{ResourceAttributes: &authv1.ResourceAttributes{ Namespace: namespace, Verb: verb, Group: gvr.Group, Version: gvr.Version, Resource: gvr.Resource, }}, }, metav1.CreateOptions{}) if err != nil { // TODO: maybe return the error too return false } return response.Status.Allowed }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/containers/kubernetes-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server