Repository: helm/helm (go)

Issue:
Add function to create configuration for actions in pkg
I'm playing around with the v3 beta, and trying to refactor the terraform provider. There doesn't seem to be an easy way to create a configuration, and I feel like I am duplicating a bunch of logic from initActionConfig in cmd/helm. It would be super helpful if there some methods added to build the configuration object in pkg



Pull request description:
Moving initActionConfig to pkg
Moved initActionConfig to pkg to make it easier to instantiate the configuration for projects consuming helm

closes #6333 

Signed-off-by: Aaron Mell <amell@lumindigital.com>

**What this PR does / why we need it**:
I was trying migrate the terraform helm provider to v3 beta. Realized that I was basically copying most of the logic in cmd/helm/helm.go to instantiate the configuration. Figured other people would have to do the same, and it should be easier to create the config without knowing about the internals of helm. 

**Special notes for your reviewer**:
I'm pretty new to go, so I'd appreciate candid feedback.

**If applicable**:
- [ ] this PR contains documentation
- [ ] this PR contains unit tests
- [ ] this PR has been tested for backwards compatibility


Patch:
diff --git a/cmd/helm/helm.go b/cmd/helm/helm.go
index 509ab3241..c309edf71 100644
--- a/cmd/helm/helm.go
+++ b/cmd/helm/helm.go
@@ -22,11 +22,9 @@ import (
 	"log"
 	"os"
 	"strings"
-	"sync"
 
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
-	"k8s.io/cli-runtime/pkg/genericclioptions"
 	"k8s.io/klog"
 
 	// Import to initialize client auth plugins.
@@ -35,18 +33,13 @@ import (
 	"helm.sh/helm/v3/pkg/action"
 	"helm.sh/helm/v3/pkg/cli"
 	"helm.sh/helm/v3/pkg/gates"
-	"helm.sh/helm/v3/pkg/kube"
-	"helm.sh/helm/v3/pkg/storage"
-	"helm.sh/helm/v3/pkg/storage/driver"
 )
 
 // FeatureGateOCI is the feature gate for checking if `helm chart` and `helm registry` commands should work
 const FeatureGateOCI = gates.Gate("HELM_EXPERIMENTAL_OCI")
 
 var (
-	settings   = cli.New()
-	config     genericclioptions.RESTClientGetter
-	configOnce sync.Once
+	settings = cli.New()
 )
 
 func init() {
@@ -71,13 +64,9 @@ func initKubeLogs() {
 func main() {
 	initKubeLogs()
 
-	actionConfig := new(action.Configuration)
-	cmd := newRootCmd(actionConfig, os.Stdout, os.Args[1:])
-
-	// Initialize the rest of the actionConfig
-	initActionConfig(actionConfig, false)
+	actionConfig, err := action.InitActionConfig(settings, false, os.Getenv("HELM_DRIVER"), debug)
 
-	if err := cmd.Execute(); err != nil {
+	if err != nil {
 		debug("%+v", err)
 		switch e := err.(type) {
 		case pluginError:
@@ -86,62 +75,13 @@ func main() {
 			os.Exit(1)
 		}
 	}
-}
-
-func initActionConfig(actionConfig *action.Configuration, allNamespaces bool) {
-	kc := kube.New(kubeConfig())
-	kc.Log = debug
-
-	clientset, err := kc.Factory.KubernetesClientSet()
-	if err != nil {
-		// TODO return error
-		log.Fatal(err)
-	}
-	var namespace string
-	if !allNamespaces {
-		namespace = getNamespace()
-	}
 
-	var store *storage.Storage
-	switch os.Getenv("HELM_DRIVER") {
-	case "secret", "secrets", "":
-		d := driver.NewSecrets(clientset.CoreV1().Secrets(namespace))
-		d.Log = debug
-		store = storage.Init(d)
-	case "configmap", "configmaps":
-		d := driver.NewConfigMaps(clientset.CoreV1().ConfigMaps(namespace))
-		d.Log = debug
-		store = storage.Init(d)
-	case "memory":
-		d := driver.NewMemory()
-		store = storage.Init(d)
-	default:
-		// Not sure what to do here.
-		panic("Unknown driver in HELM_DRIVER: " + os.Getenv("HELM_DRIVER"))
-	}
-
-	actionConfig.RESTClientGetter = kubeConfig()
-	actionConfig.KubeClient = kc
-	actionConfig.Releases = store
-	actionConfig.Log = debug
-}
-
-func kubeConfig() genericclioptions.RESTClientGetter {
-	configOnce.Do(func() {
-		config = kube.GetConfig(settings.KubeConfig, settings.KubeContext, settings.Namespace)
-	})
-	return config
-}
-
-func getNamespace() string {
-	if settings.Namespace != "" {
-		return settings.Namespace
-	}
+	cmd := newRootCmd(actionConfig, os.Stdout, os.Args[1:])
 
-	if ns, _, err := kubeConfig().ToRawKubeConfigLoader().Namespace(); err == nil {
-		return ns
+	if err := cmd.Execute(); err != nil {
+		debug("%+v", err)
+		os.Exit(1)
 	}
-	return "default"
 }
 
 // wordSepNormalizeFunc changes all flags that contain "_" separators
diff --git a/cmd/helm/install.go b/cmd/helm/install.go
index 2540558a5..e29fbbda1 100644
--- a/cmd/helm/install.go
+++ b/cmd/helm/install.go
@@ -205,7 +205,7 @@ func runInstall(args []string, client *action.Install, valueOpts *values.Options
 		}
 	}
 
-	client.Namespace = getNamespace()
+	client.Namespace = settings.Namespace()
 	return client.Run(chartRequested, vals)
 }
 
diff --git a/cmd/helm/lint.go b/cmd/helm/lint.go
index 13395c6d6..9a2e8d31c 100644
--- a/cmd/helm/lint.go
+++ b/cmd/helm/lint.go
@@ -51,7 +51,7 @@ func newLintCmd(out io.Writer) *cobra.Command {
 			if len(args) > 0 {
 				paths = args
 			}
-			client.Namespace = getNamespace()
+			client.Namespace = settings.Namespace()
 			vals, err := valueOpts.MergeValues(getter.All(settings))
 			if err != nil {
 				return err
diff --git a/cmd/helm/list.go b/cmd/helm/list.go
index 8c89425f5..50f9a7441 100644
--- a/cmd/helm/list.go
+++ b/cmd/helm/list.go
@@ -19,6 +19,7 @@ package main
 import (
 	"fmt"
 	"io"
+	"os"
 	"strconv"
 
 	"github.com/gosuri/uitable"
@@ -69,7 +70,7 @@ func newListCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 		Args:    require.NoArgs,
 		RunE: func(cmd *cobra.Command, args []string) error {
 			if client.AllNamespaces {
-				initActionConfig(cfg, true)
+				action.InitActionConfig(settings, true, os.Getenv("HELM_DRIVER"), debug)
 			}
 			client.SetStateMask()
 
diff --git a/cmd/helm/upgrade.go b/cmd/helm/upgrade.go
index 33631504f..22dd23970 100644
--- a/cmd/helm/upgrade.go
+++ b/cmd/helm/upgrade.go
@@ -71,7 +71,7 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 		Long:  upgradeDesc,
 		Args:  require.ExactArgs(2),
 		RunE: func(cmd *cobra.Command, args []string) error {
-			client.Namespace = getNamespace()
+			client.Namespace = settings.Namespace()
 
 			if client.Version == "" && client.Devel {
 				debug("setting version to >0.0.0-0")
diff --git a/pkg/action/action.go b/pkg/action/action.go
index f784b3651..7cfe4a7d3 100644
--- a/pkg/action/action.go
+++ b/pkg/action/action.go
@@ -29,9 +29,11 @@ import (
 
 	"helm.sh/helm/v3/internal/experimental/registry"
 	"helm.sh/helm/v3/pkg/chartutil"
+	"helm.sh/helm/v3/pkg/cli"
 	"helm.sh/helm/v3/pkg/kube"
 	"helm.sh/helm/v3/pkg/release"
 	"helm.sh/helm/v3/pkg/storage"
+	"helm.sh/helm/v3/pkg/storage/driver"
 )
 
 // Timestamper is a function capable of producing a timestamp.Timestamper.
@@ -82,6 +84,16 @@ type Configuration struct {
 	Log func(string, ...interface{})
 }
 
+// RESTClientGetter gets the rest client
+type RESTClientGetter interface {
+	ToRESTConfig() (*rest.Config, error)
+	ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error)
+	ToRESTMapper() (meta.RESTMapper, error)
+}
+
+// DebugLog sets the logger that writes debug strings
+type DebugLog func(format string, v ...interface{})
+
 // capabilities builds a Capabilities from discovery information.
 func (c *Configuration) getCapabilities() (*chartutil.Capabilities, error) {
 	if c.Capabilities != nil {
@@ -197,8 +209,46 @@ func (c *Configuration) recordRelease(r *release.Release) {
 	}
 }
 
-type RESTClientGetter interface {
-	ToRESTConfig() (*rest.Config, error)
-	ToDiscoveryClient() (discovery.CachedDiscoveryInterface, error)
-	ToRESTMapper() (meta.RESTMapper, error)
+// InitActionConfig initializes the action configuration
+func InitActionConfig(envSettings *cli.EnvSettings, allNamespaces bool, helmDriver string, log DebugLog) (*Configuration, error) {
+
+	var actionConfig Configuration
+	kubeconfig := envSettings.KubeConfig()
+
+	kc := kube.New(kubeconfig)
+	kc.Log = log
+
+	clientset, err := kc.Factory.KubernetesClientSet()
+	if err != nil {
+		return nil, err
+	}
+	var namespace string
+	if !allNamespaces {
+		namespace = envSettings.Namespace()
+	}
+
+	var store *storage.Storage
+	switch helmDriver {
+	case "secret", "secrets", "":
+		d := driver.NewSecrets(clientset.CoreV1().Secrets(namespace))
+		d.Log = log
+		store = storage.Init(d)
+	case "configmap", "configmaps":
+		d := driver.NewConfigMaps(clientset.CoreV1().ConfigMaps(namespace))
+		d.Log = log
+		store = storage.Init(d)
+	case "memory":
+		d := driver.NewMemory()
+		store = storage.Init(d)
+	default:
+		// Not sure what to do here.
+		panic("Unknown driver in HELM_DRIVER: " + helmDriver)
+	}
+
+	actionConfig.RESTClientGetter = kubeconfig
+	actionConfig.KubeClient = kc
+	actionConfig.Releases = store
+	actionConfig.Log = log
+
+	return &actionConfig, nil
 }
diff --git a/pkg/cli/environment.go b/pkg/cli/environment.go
index bbe3e964b..76dc31bcf 100644
--- a/pkg/cli/environment.go
+++ b/pkg/cli/environment.go
@@ -26,23 +26,26 @@ import (
 	"fmt"
 	"os"
 	"strconv"
+	"sync"
 
 	"github.com/spf13/pflag"
 
+	"k8s.io/cli-runtime/pkg/genericclioptions"
+
 	"helm.sh/helm/v3/pkg/helmpath"
+	"helm.sh/helm/v3/pkg/kube"
 )
 
 // EnvSettings describes all of the environment settings.
 type EnvSettings struct {
-	// Namespace is the namespace scope.
-	Namespace string
-	// KubeConfig is the path to the kubeconfig file.
-	KubeConfig string
+	namespace  string
+	kubeConfig string
+	config     genericclioptions.RESTClientGetter
+	configOnce sync.Once
 	// KubeContext is the name of the kubeconfig context.
 	KubeContext string
 	// Debug indicates whether or not Helm is running in Debug mode.
 	Debug bool
-
 	// RegistryConfig is the path to the registry config file.
 	RegistryConfig string
 	// RepositoryConfig is the path to the repositories file.
@@ -54,8 +57,9 @@ type EnvSettings struct {
 }
 
 func New() *EnvSettings {
+
 	env := EnvSettings{
-		Namespace:        os.Getenv("HELM_NAMESPACE"),
+		namespace:        os.Getenv("HELM_NAMESPACE"),
 		PluginsDirectory: envOr("HELM_PLUGINS", helmpath.DataPath("plugins")),
 		RegistryConfig:   envOr("HELM_REGISTRY_CONFIG", helmpath.ConfigPath("registry.json")),
 		RepositoryConfig: envOr("HELM_REPOSITORY_CONFIG", helmpath.ConfigPath("repositories.yaml")),
@@ -67,8 +71,8 @@ func New() *EnvSettings {
 
 // AddFlags binds flags to the given flagset.
 func (s *EnvSettings) AddFlags(fs *pflag.FlagSet) {
-	fs.StringVarP(&s.Namespace, "namespace", "n", s.Namespace, "namespace scope for this request")
-	fs.StringVar(&s.KubeConfig, "kubeconfig", "", "path to the kubeconfig file")
+	fs.StringVarP(&s.namespace, "namespace", "n", s.namespace, "namespace scope for this request")
+	fs.StringVar(&s.kubeConfig, "kubeconfig", "", "path to the kubeconfig file")
 	fs.StringVar(&s.KubeContext, "kube-context", "", "name of the kubeconfig context to use")
 	fs.BoolVar(&s.Debug, "debug", s.Debug, "enable verbose output")
 	fs.StringVar(&s.RegistryConfig, "registry-config", s.RegistryConfig, "path to the registry config file")
@@ -93,3 +97,23 @@ func (s *EnvSettings) EnvVars() map[string]string {
 		"HELM_REPOSITORY_CONFIG": s.RepositoryConfig,
 	}
 }
+
+//Namespace gets the namespace from the configuration
+func (s *EnvSettings) Namespace() string {
+	if s.namespace != "" {
+		return s.namespace
+	}
+
+	if ns, _, err := s.KubeConfig().ToRawKubeConfigLoader().Namespace(); err == nil {
+		return ns
+	}
+	return "default"
+}
+
+//KubeConfig gets the kubeconfig from EnvSettings
+func (s *EnvSettings) KubeConfig() genericclioptions.RESTClientGetter {
+	s.configOnce.Do(func() {
+		s.config = kube.GetConfig(s.kubeConfig, s.KubeContext, s.namespace)
+	})
+	return s.config
+}


Test patch:
diff --git a/pkg/cli/environment_test.go b/pkg/cli/environment_test.go
index 58cf1c7f4..d6856dd01 100644
--- a/pkg/cli/environment_test.go
+++ b/pkg/cli/environment_test.go
@@ -38,7 +38,7 @@ func TestEnvSettings(t *testing.T) {
 	}{
 		{
 			name: "defaults",
-			ns:   "",
+			ns:   "default",
 		},
 		{
 			name:  "with flags set",
@@ -78,8 +78,8 @@ func TestEnvSettings(t *testing.T) {
 			if settings.Debug != tt.debug {
 				t.Errorf("expected debug %t, got %t", tt.debug, settings.Debug)
 			}
-			if settings.Namespace != tt.ns {
-				t.Errorf("expected namespace %q, got %q", tt.ns, settings.Namespace)
+			if settings.Namespace() != tt.ns {
+				t.Errorf("expected namespace %q, got %q", tt.ns, settings.Namespace())
 			}
 			if settings.KubeContext != tt.kcontext {
 				t.Errorf("expected kube-context %q, got %q", tt.kcontext, settings.KubeContext)