Skip to main content
Glama
client.go12 kB
package helm import ( "context" "fmt" "helm.sh/helm/v3/pkg/action" "helm.sh/helm/v3/pkg/chart/loader" "helm.sh/helm/v3/pkg/cli" "helm.sh/helm/v3/pkg/getter" "helm.sh/helm/v3/pkg/registry" "helm.sh/helm/v3/pkg/release" "helm.sh/helm/v3/pkg/repo" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/client-go/discovery" "k8s.io/client-go/discovery/cached/memory" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/restmapper" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd/api" "log" "os" "path/filepath" "github.com/reza-gholizade/k8s-mcp-server/pkg/k8s" ) // Client wraps Helm operations type Client struct { settings *cli.EnvSettings restConfig *rest.Config k8sClient kubernetes.Interface restClientGetter genericclioptions.RESTClientGetter } // customRESTClientGetter is a custom RESTClientGetter that uses a pre-built rest.Config // instead of reading from kubeconfig files. This ensures Helm uses the same authentication // method that was used to build the restConfig (KUBECONFIG_DATA, KUBERNETES_SERVER/TOKEN, etc.) type customRESTClientGetter struct { restConfig *rest.Config } // ToRESTConfig returns the pre-built REST config func (g *customRESTClientGetter) ToRESTConfig() (*rest.Config, error) { return g.restConfig, nil } // ToRawKubeConfigLoader returns a clientcmd.ClientConfig that uses the pre-built config func (g *customRESTClientGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig { return &customClientConfig{restConfig: g.restConfig} } // ToDiscoveryClient returns a discovery client using the pre-built REST config func (g *customRESTClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) { discoveryClient, err := discovery.NewDiscoveryClientForConfig(g.restConfig) if err != nil { return nil, err } return memory.NewMemCacheClient(discoveryClient), nil } // ToRESTMapper returns a REST mapper using the discovery client func (g *customRESTClientGetter) ToRESTMapper() (meta.RESTMapper, error) { discoveryClient, err := g.ToDiscoveryClient() if err != nil { return nil, err } mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient) expander := restmapper.NewShortcutExpander(mapper, discoveryClient, nil) return expander, nil } // customClientConfig implements clientcmd.ClientConfig interface type customClientConfig struct { restConfig *rest.Config } // RawConfig returns an empty api.Config since we're using a direct rest.Config func (c *customClientConfig) RawConfig() (api.Config, error) { return api.Config{}, fmt.Errorf("raw config not available when using direct REST config") } // ClientConfig returns the pre-built REST config func (c *customClientConfig) ClientConfig() (*rest.Config, error) { return c.restConfig, nil } // Namespace returns the default namespace from the config func (c *customClientConfig) Namespace() (string, bool, error) { return "default", false, nil } // ConfigAccess returns nil as we don't use file-based config access func (c *customClientConfig) ConfigAccess() clientcmd.ConfigAccess { return nil } // NewClient creates a new Helm client. // It uses the same authentication methods as the Kubernetes client: // 1. Kubeconfig content from KUBECONFIG_DATA environment variable // 2. API server URL and token from KUBERNETES_SERVER and KUBERNETES_TOKEN environment variables // 3. In-cluster authentication (service account token) // 4. Kubeconfig file path (provided or default ~/.kube/config) func NewClient(kubeconfig string) (*Client, error) { settings := cli.New() // Get Kubernetes REST config using the shared config builder restConfig, err := k8s.BuildKubernetesConfig(kubeconfig) if err != nil { return nil, fmt.Errorf("failed to get Kubernetes config: %w", err) } // Create a custom RESTClientGetter that uses our pre-built restConfig // This ensures Helm uses the same authentication method (KUBECONFIG_DATA, // KUBERNETES_SERVER/TOKEN, in-cluster, etc.) instead of trying to read from // settings.KubeConfig which may not be set or may point to a different config. restClientGetter := &customRESTClientGetter{restConfig: restConfig} // Set kubeconfig path in settings if provided (for Helm's internal use in other contexts) // Note: This is mainly for compatibility, but Helm operations will use restClientGetter if kubeconfig != "" { settings.KubeConfig = kubeconfig } else if kubeconfigEnv := os.Getenv("KUBECONFIG"); kubeconfigEnv != "" { settings.KubeConfig = kubeconfigEnv } // Create Kubernetes client k8sClient, err := kubernetes.NewForConfig(restConfig) if err != nil { return nil, fmt.Errorf("failed to create Kubernetes client: %w", err) } return &Client{ settings: settings, restConfig: restConfig, k8sClient: k8sClient, restClientGetter: restClientGetter, }, nil } func (c *Client) InstallChart(ctx context.Context, namespace, releaseName, chartName, repoURL string, values map[string]interface{}) (*release.Release, error) { actionConfig := &action.Configuration{} if err := actionConfig.Init(c.restClientGetter, namespace, os.Getenv("HELM_DRIVER"), log.Printf); err != nil { return nil, fmt.Errorf("failed to initialize action config: %w", err) } client := action.NewInstall(actionConfig) client.Namespace = namespace client.ReleaseName = releaseName client.CreateNamespace = true cln, err := registry.NewClient( registry.ClientOptDebug(true), registry.ClientOptCredentialsFile(""), registry.ClientOptEnableCache(false)) if err != nil { return nil, fmt.Errorf("failed to initialize registry: %w", err) } fmt.Println("Registry client created successfully:", cln) if values == nil { values = make(map[string]interface{}) } // If repoURL is provided, add it to settings or append to chartName accordingly if repoURL != "" { client.RepoURL = repoURL } // Locate the chart (resolves repo/chart or OCI) chartPath, err := client.LocateChart(chartName, c.settings) if err != nil { return nil, fmt.Errorf("failed to locate chart: %w", err) } // Load the chart from the resolved path (can be a URL or OCI reference) chart, err := loader.Load(chartPath) if err != nil { return nil, fmt.Errorf("failed to load chart: %w", err) } // Run the install action release, err := client.Run(chart, values) if err != nil { return nil, fmt.Errorf("failed to install chart: %w", err) } return release, nil } func (c *Client) UpgradeChart(ctx context.Context, namespace, releaseName, chartName string, values map[string]interface{}) (*release.Release, error) { actionConfig := &action.Configuration{} if err := actionConfig.Init(c.restClientGetter, namespace, os.Getenv("HELM_DRIVER"), log.Printf); err != nil { return nil, fmt.Errorf("failed to initialize action config: %w", err) } // Create and assign registry client regClient, err := registry.NewClient( registry.ClientOptDebug(true), registry.ClientOptEnableCache(false), ) if err != nil { return nil, fmt.Errorf("failed to initialize registry client: %w", err) } fmt.Println("Registry client created successfully:", regClient) client := action.NewUpgrade(actionConfig) client.Namespace = namespace if values == nil { values = make(map[string]interface{}) } // Locate the chart (for both OCI and regular charts) chartPath, err := client.LocateChart(chartName, c.settings) if err != nil { return nil, fmt.Errorf("failed to locate chart: %w", err) } chart, err := loader.Load(chartPath) if err != nil { return nil, fmt.Errorf("failed to load chart: %w", err) } release, err := client.Run(releaseName, chart, values) if err != nil { return nil, fmt.Errorf("failed to upgrade chart: %w", err) } return release, nil } // UninstallChart uninstalls a Helm release func (c *Client) UninstallChart(ctx context.Context, namespace, releaseName string) error { actionConfig := &action.Configuration{} if err := actionConfig.Init(c.restClientGetter, namespace, os.Getenv("HELM_DRIVER"), log.Printf); err != nil { return fmt.Errorf("failed to initialize action config: %w", err) } client := action.NewUninstall(actionConfig) _, err := client.Run(releaseName) if err != nil { return fmt.Errorf("failed to uninstall release: %w", err) } return nil } func (c *Client) ListReleases(ctx context.Context, namespace string) ([]*release.Release, error) { actionConfig := &action.Configuration{} if err := actionConfig.Init(c.restClientGetter, namespace, os.Getenv("HELM_DRIVER"), log.Printf); err != nil { return nil, fmt.Errorf("failed to initialize action config: %w", err) } client := action.NewList(actionConfig) client.AllNamespaces = namespace == "" releases, err := client.Run() if err != nil { return nil, fmt.Errorf("failed to list releases: %w", err) } // remove useless fields from releases for _, release := range releases { release.Chart.Templates = nil release.Chart.Files = nil release.Chart.Values = nil release.Chart.Schema = nil release.Config = nil release.Manifest = "" release.Chart.Lock = nil release.Hooks = nil } return releases, nil } func (c *Client) GetRelease(ctx context.Context, namespace, releaseName string) (*release.Release, error) { actionConfig := &action.Configuration{} if err := actionConfig.Init(c.restClientGetter, namespace, os.Getenv("HELM_DRIVER"), log.Printf); err != nil { return nil, fmt.Errorf("failed to initialize action config: %w", err) } client := action.NewGet(actionConfig) release, err := client.Run(releaseName) if err != nil { return nil, fmt.Errorf("failed to get release: %w", err) } return release, nil } func (c *Client) GetReleaseHistory(ctx context.Context, namespace, releaseName string) ([]*release.Release, error) { actionConfig := &action.Configuration{} if err := actionConfig.Init(c.restClientGetter, namespace, os.Getenv("HELM_DRIVER"), log.Printf); err != nil { return nil, fmt.Errorf("failed to initialize action config: %w", err) } client := action.NewHistory(actionConfig) releases, err := client.Run(releaseName) if err != nil { return nil, fmt.Errorf("failed to get release history: %w", err) } return releases, nil } // RollbackRelease rolls back a Helm release func (c *Client) RollbackRelease(ctx context.Context, namespace, releaseName string, revision int) error { actionConfig := &action.Configuration{} if err := actionConfig.Init(c.restClientGetter, namespace, os.Getenv("HELM_DRIVER"), log.Printf); err != nil { return fmt.Errorf("failed to initialize action config: %w", err) } client := action.NewRollback(actionConfig) client.Version = revision if err := client.Run(releaseName); err != nil { return fmt.Errorf("failed to rollback release: %w", err) } return nil } // addRepo adds a Helm repository func (c *Client) HelmRepoAdd(ctx context.Context, name, url string) error { repoFile := c.settings.RepositoryConfig // Ensure the file directory exists if err := os.MkdirAll(filepath.Dir(repoFile), 0755); err != nil { return err } // Load existing repositories f, err := repo.LoadFile(repoFile) if err != nil && !os.IsNotExist(err) { return err } if f == nil { f = repo.NewFile() } // Check if repo already exists if f.Has(name) { return nil // Already exists } // Add the repository entry := &repo.Entry{ Name: name, URL: url, } r, err := repo.NewChartRepository(entry, getter.All(c.settings)) if err != nil { return err } if _, err := r.DownloadIndexFile(); err != nil { return fmt.Errorf("failed to download repository index: %w", err) } f.Update(entry) return f.WriteFile(repoFile, 0644) } func (c *Client) HelmRepoList(ctx context.Context) ([]*repo.Entry, error) { repoFile := c.settings.RepositoryConfig f, err := repo.LoadFile(repoFile) if err != nil { return nil, fmt.Errorf("failed to load repository file: %w", err) } return f.Repositories, nil }

Latest Blog Posts

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/reza-gholizade/k8s-mcp-server'

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