Skip to content
98 changes: 9 additions & 89 deletions utils/kubernetes/apply-helm-chart.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ type ApplyHelmChartConfig struct {
// },
// OverrideValues: vals,
// })
func (client *Client) ApplyHelmChart(cfg ApplyHelmChartConfig) error {
func (c *Client) ApplyHelmChart(cfg ApplyHelmChartConfig) error {
setupDefaults(&cfg)

if err := setupChartVersion(&cfg); err != nil {
Expand All @@ -264,11 +264,10 @@ func (client *Client) ApplyHelmChart(cfg ApplyHelmChartConfig) error {
return ErrApplyHelmChart(err)
}

actionConfig, cleanup, err := createHelmActionConfig(client, cfg)
actionConfig, err := c.createHelmActionConfig(cfg, c.getRESTClientGetter())
if err != nil {
return ErrApplyHelmChart(err)
}
defer cleanup()

// Before installing a helm chart, check if it already exists in the cluster
// this is a workaround make the helm chart installation idempotent
Expand Down Expand Up @@ -408,96 +407,16 @@ func checkIfInstallable(ch *chart.Chart) error {
}

// createHelmActionConfig generates the actionConfig with the appropriate defaults
func createHelmActionConfig(c *Client, cfg ApplyHelmChartConfig) (*action.Configuration, func(), error) {
func (c *Client) createHelmActionConfig(cfg ApplyHelmChartConfig, restClientGetter genericclioptions.RESTClientGetter) (*action.Configuration, error) {
// Set the environment variable needed by the Init methods
_ = os.Setenv("HELM_DRIVER_SQL_CONNECTION_STRING", cfg.SQLConnectionString)
Comment on lines 411 to 412

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

os.Setenv mutates process-global state and now leaks into exec credential plugins.

Three problems in this line:

  1. The call is not concurrency safe. Two concurrent ApplyHelmChart calls with different SQLConnectionString values race on the same process environment.
  2. If cfg.SQLConnectionString is empty, the call clears any value that was set earlier in the process.
  3. This PR makes Helm spawn exec credential plugins, for example aws eks get-token. Child processes inherit the process environment, so the SQL connection string, including any embedded password, is now exposed to those plugins.

Set the variable only when cfg.HelmDriver is the SQL driver and cfg.SQLConnectionString is not empty. Also check the returned error.

🔐 Proposed fix
-	// Set the environment variable needed by the Init methods
-	_ = os.Setenv("HELM_DRIVER_SQL_CONNECTION_STRING", cfg.SQLConnectionString)
+	// Set the environment variable needed by the Init methods.
+	// Only set it when a connection string is supplied, so an empty value does not
+	// clear a previously configured one and does not reach spawned credential plugins.
+	if cfg.SQLConnectionString != "" {
+		if err := os.Setenv("HELM_DRIVER_SQL_CONNECTION_STRING", cfg.SQLConnectionString); err != nil {
+			return nil, err
+		}
+	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@utils/kubernetes/apply-helm-chart.go` around lines 411 - 412, Update the
environment setup in ApplyHelmChart to avoid mutating process-global state: only
configure HELM_DRIVER_SQL_CONNECTION_STRING when cfg.HelmDriver is the SQL
driver and cfg.SQLConnectionString is non-empty, and handle the error returned
by os.Setenv. Ensure the value is scoped to Helm and is not inherited by exec
credential plugins or concurrent ApplyHelmChart calls.


var tempFiles []string
cleanup := func() {
for _, f := range tempFiles {
_ = os.Remove(f)
}
}

// KubeConfig setup
kubeConfig := genericclioptions.NewConfigFlags(false)
// Set KubeConfig to DevNull to prevent read from local kubeconfig
// to prevent conflicts between "data" and "files" properties (CAFile, CAData and KeyFile, KeyData)
// ConfigFlags only allows setting CAFile, KeyFile but not CAData, KeyData.
// When the library reads the original kubeconfig containing cert data / key data AND we specify cert file / key file, these configurations conflict
devNull := os.DevNull
kubeConfig.KubeConfig = &devNull
kubeConfig.APIServer = &c.RestConfig.Host
kubeConfig.BearerToken = &c.RestConfig.BearerToken
kubeConfig.Insecure = &c.RestConfig.Insecure

// Set username and password for basic auth if available
if c.RestConfig.Username != "" {
kubeConfig.Username = &c.RestConfig.Username
}
if c.RestConfig.Password != "" {
kubeConfig.Password = &c.RestConfig.Password
}

// Only set CA file if not running in insecure mode
if !c.RestConfig.Insecure {
if len(c.RestConfig.CAData) > 0 {
caFileName, err := setDataAndReturnFilename(c.RestConfig.CAData)
if err != nil {
cleanup() // Clean up any files created so far
return nil, nil, err
}
tempFiles = append(tempFiles, caFileName)
kubeConfig.CAFile = &caFileName
}
}

// Set client certificate data if available
if len(c.RestConfig.CertData) > 0 {
certFileName, err := setDataAndReturnFilename(c.RestConfig.CertData)
if err != nil {
cleanup()
return nil, nil, err
}
tempFiles = append(tempFiles, certFileName)
kubeConfig.CertFile = &certFileName
}

// Set client key data if available
if len(c.RestConfig.KeyData) > 0 {
keyFileName, err := setDataAndReturnFilename(c.RestConfig.KeyData)
if err != nil {
cleanup() // Clean up any files created so far
return nil, nil, err
}
tempFiles = append(tempFiles, keyFileName)
kubeConfig.KeyFile = &keyFileName
}

actionConfig := new(action.Configuration)
if err := actionConfig.Init(kubeConfig, cfg.Namespace, string(cfg.HelmDriver), cfg.Logger); err != nil {
cleanup() // Clean up any files created so far
return nil, nil, ErrApplyHelmChart(err)
if err := actionConfig.Init(restClientGetter, cfg.Namespace, string(cfg.HelmDriver), cfg.Logger); err != nil {
return nil, ErrApplyHelmChart(err)
}

return actionConfig, cleanup, nil
}

// Populates a file in temp directory with the passed data and returns the filename
func setDataAndReturnFilename(data []byte) (string, error) {
f, err := os.CreateTemp("", "")
if err != nil {
return "", err
}
defer func() { _ = f.Close() }() // Close file immediately after writing

_, err = f.Write(data)
if err != nil {
_ = os.Remove(f.Name()) // Clean up on write error
return "", err
}

return f.Name(), nil
return actionConfig, nil
}

// generateAction generates an action function using action.Configuration
Expand Down Expand Up @@ -554,7 +473,8 @@ func createHelmPathFromHelmChartLocation(loc HelmChartLocation) (string, error)
getter.Provider{
Schemes: []string{"http", "https"},
New: getter.NewHTTPGetter,
}},
},
},
)
if err != nil {
return "", ErrApplyHelmChart(err)
Expand Down Expand Up @@ -643,7 +563,7 @@ func (helmEntries HelmEntries) GetEntryWithAppVersion(entry, appVersion string)
return HelmEntryMetadata{}, false
}

// GetEntryWithAppVersion takes in the entry name and the appversion and returns the corresponding
// GetEntryWithChartVersion takes in the entry name and the appversion and returns the corresponding

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The comment still says "appversion".

The function is GetEntryWithChartVersion. The comment describes the second parameter as "the appversion". Update the prose to "the chart version" so it matches the function name.

📝 Proposed fix
-// GetEntryWithChartVersion takes in the entry name and the appversion and returns the corresponding
+// GetEntryWithChartVersion takes in the entry name and the chart version and returns the corresponding
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// GetEntryWithChartVersion takes in the entry name and the appversion and returns the corresponding
// GetEntryWithChartVersion takes in the entry name and the chart version and returns the corresponding
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@utils/kubernetes/apply-helm-chart.go` at line 566, Update the comment for
GetEntryWithChartVersion so the second parameter is described as “the chart
version” instead of “the appversion,” keeping the rest of the documentation
unchanged.

// metadata for the parameters if it exists
func (helmEntries HelmEntries) GetEntryWithChartVersion(entry, chartVersion string) (HelmEntryMetadata, bool) {
hem, ok := helmEntries[entry]
Expand Down
101 changes: 101 additions & 0 deletions utils/kubernetes/client-config-getter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package kubernetes

import (
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/discovery"
"k8s.io/client-go/discovery/cached/memory"
"k8s.io/client-go/rest"
"k8s.io/client-go/restmapper"
"k8s.io/client-go/tools/clientcmd"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
)

type clientConfigRESTClientGetter struct {
clientConfig clientcmd.ClientConfig
}

type restConfigClientConfig struct {
restConfig *rest.Config
}

var _ genericclioptions.RESTClientGetter = (*clientConfigRESTClientGetter)(nil)
var _ clientcmd.ClientConfig = (*restConfigClientConfig)(nil)

func newClientConfigRESTClientGetter(clientConfig clientcmd.ClientConfig) genericclioptions.RESTClientGetter {
return &clientConfigRESTClientGetter{clientConfig: clientConfig}
}

func newRESTConfigRESTClientGetter(config *rest.Config) genericclioptions.RESTClientGetter {
return newClientConfigRESTClientGetter(&restConfigClientConfig{
restConfig: rest.CopyConfig(config),
})
}

func (g *clientConfigRESTClientGetter) ToRESTConfig() (*rest.Config, error) {
config, err := g.clientConfig.ClientConfig()
if err != nil {
return nil, err
}
configureRESTConfig(config)
return config, nil
}

func (g *clientConfigRESTClientGetter) ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error) {
config, err := g.ToRESTConfig()
if err != nil {
return nil, err
}

discoveryClient, err := discovery.NewDiscoveryClientForConfig(config)
if err != nil {
return nil, err
}

return memory.NewMemCacheClient(discoveryClient), nil
}

func (g *clientConfigRESTClientGetter) ToRESTMapper() (meta.RESTMapper, error) {
discoveryClient, err := g.ToDiscoveryClient()
if err != nil {
return nil, err
}

mapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)
return restmapper.NewShortcutExpander(mapper, discoveryClient, func(string) {}), nil
}

func (g *clientConfigRESTClientGetter) ToRawKubeConfigLoader() clientcmd.ClientConfig {
return g.clientConfig
}

func (c *restConfigClientConfig) RawConfig() (clientcmdapi.Config, error) {
const connectionName = "meshkit-connection"

config := clientcmdapi.NewConfig()
config.Clusters[connectionName] = &clientcmdapi.Cluster{
Server: c.restConfig.Host,
TLSServerName: c.restConfig.ServerName,
InsecureSkipTLSVerify: c.restConfig.Insecure,
CertificateAuthority: c.restConfig.CAFile,
CertificateAuthorityData: c.restConfig.CAData,
DisableCompression: c.restConfig.DisableCompression,
}
config.Contexts[connectionName] = &clientcmdapi.Context{
Cluster: connectionName,
}
config.CurrentContext = connectionName
return *config, nil
}
Comment on lines +72 to +89

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

RawConfig drops the authentication data of the REST config.

The synthetic config contains a cluster and a context, but no AuthInfo. It omits BearerToken, BearerTokenFile, client certificate and key data, Username/Password, ExecProvider, and Proxy. Consumers that build clients from RawConfig instead of ClientConfig receive an unauthenticated configuration. Helm reads the namespace and context from this path today, so the impact is limited, but the omission is silent.

Add an AuthInfo that mirrors the REST config credentials, and link the context to it.

🔐 Proposed fix to preserve credentials in RawConfig
 	config.Contexts[connectionName] = &clientcmdapi.Context{
 		Cluster: connectionName,
+		AuthInfo: connectionName,
 	}
+	config.AuthInfos[connectionName] = &clientcmdapi.AuthInfo{
+		Token:                 c.restConfig.BearerToken,
+		TokenFile:             c.restConfig.BearerTokenFile,
+		ClientCertificate:     c.restConfig.CertFile,
+		ClientCertificateData: c.restConfig.CertData,
+		ClientKey:             c.restConfig.KeyFile,
+		ClientKeyData:         c.restConfig.KeyData,
+		Username:              c.restConfig.Username,
+		Password:              c.restConfig.Password,
+		Exec:                  c.restConfig.ExecProvider,
+	}
 	config.CurrentContext = connectionName
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@utils/kubernetes/client-config-getter.go` around lines 72 - 89, Update
restConfigClientConfig.RawConfig to add an AuthInfo entry for connectionName
that mirrors the REST config authentication fields, including bearer token/file,
client certificate/key data, username/password, exec provider, and proxy
settings. Link the existing context’s AuthInfo field to connectionName while
preserving the current cluster and context configuration.


func (c *restConfigClientConfig) ClientConfig() (*rest.Config, error) {
return rest.CopyConfig(c.restConfig), nil
}

func (c *restConfigClientConfig) Namespace() (string, bool, error) {
return "default", false, nil
}

func (c *restConfigClientConfig) ConfigAccess() clientcmd.ConfigAccess {
return nil
}
Loading