Repository: FairwindsOps/pluto (go)

Issue:
Add a "--kubeconfig" global flag
**Is your feature request related to a problem? Please describe.**
I often work with multiple clusters. In my case, each has a separate kubeconfig file.
Often this is because clusters are spun up for testing using e.g. vagrant+Ansible.

Having to export `KUBECONFIG=...` each time I need to target a different cluster is error prone and tedious.

**Describe the solution you'd like**
Other tools, including kubectl and helm, allow using a `--kubeconfig` global flag, that uses the file given as argument to this flag as the kubeconfig for the command.

**Describe alternatives you've considered**
- exporting `KUBECONFIG=...`
- using things like kubie etc.


Pull request description:
Bensky/kubeconfig flag
This PR fixes #540

## Checklist
* [X] I have signed the CLA
* [X] I have updated/added any relevant documentation

## Description
### What's the goal of this PR?

Enable `--kubeconfig` flag to be passed in with `pluto` commands.

### What changes did you make?

Added the `--kubeconfig` flag as an option for the commands that require kubectl clients.

### What alternative solution should we consider, if any?



Patch:
diff --git a/cmd/root.go b/cmd/root.go
index 6faa65e..52a741c 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -39,7 +39,7 @@ import (
 	discoveryapi "github.com/fairwindsops/pluto/v5/pkg/discovery-api"
 	"github.com/fairwindsops/pluto/v5/pkg/finder"
 	"github.com/fairwindsops/pluto/v5/pkg/helm"
-	"github.com/rogpeppe/go-internal/semver"
+	"golang.org/x/mod/semver"
 
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
@@ -67,6 +67,7 @@ var (
 	noHeaders                     bool
 	exitCode                      int
 	noFooter                      bool
+	kubeConfigPath                string
 )
 
 const (
@@ -100,14 +101,17 @@ func init() {
 	detectFilesCmd.PersistentFlags().StringVarP(&directory, "directory", "d", "", "The directory to scan. If blank, defaults to current working dir.")
 
 	rootCmd.AddCommand(detectHelmCmd)
+	detectHelmCmd.PersistentFlags().StringVarP(&kubeConfigPath, "kubeconfig", "", "", "The path to the kubeconfig file to use. If blank, defaults to current kubeconfig.")
 	detectHelmCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "", "Only detect releases in a specific namespace.")
 	detectHelmCmd.PersistentFlags().StringVar(&kubeContext, "kube-context", "", "The kube context to use. If blank, defaults to current context.")
 
 	rootCmd.AddCommand(detectApiResourceCmd)
+	detectApiResourceCmd.PersistentFlags().StringVarP(&kubeConfigPath, "kubeconfig", "", "", "The path to the kubeconfig file to use. If blank, defaults to current kubeconfig.")
 	detectApiResourceCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "", "Only detect resources in a specific namespace.")
 	detectApiResourceCmd.PersistentFlags().StringVar(&kubeContext, "kube-context", "", "The kube context to use. If blank, defaults to current context.")
 
 	rootCmd.AddCommand(detectAllInClusterCmd)
+	detectAllInClusterCmd.PersistentFlags().StringVarP(&kubeConfigPath, "kubeconfig", "", "", "The path to the kubeconfig file to use. If blank, defaults to current kubeconfig.")
 	detectAllInClusterCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "", "Only detect resources in a specific namespace.")
 	detectAllInClusterCmd.PersistentFlags().StringVar(&kubeContext, "kube-context", "", "The kube context to use. If blank, defaults to current context.")
 
@@ -475,7 +479,7 @@ func Execute(VERSION string, COMMIT string, versionsFile []byte) {
 }
 
 func detectHelm() error {
-	h, err := helm.NewHelm(namespace, kubeContext, apiInstance)
+	h, err := helm.NewHelm(namespace, kubeContext, apiInstance, kubeConfigPath)
 	if err != nil {
 		return fmt.Errorf("error getting helm configuration: %v", err)
 	}
@@ -487,7 +491,7 @@ func detectHelm() error {
 }
 
 func detectAPIResources() error {
-	disCl, err := discoveryapi.NewDiscoveryClient(namespace, kubeContext, apiInstance)
+	disCl, err := discoveryapi.NewDiscoveryClient(namespace, kubeContext, apiInstance, kubeConfigPath)
 	if err != nil {
 		return fmt.Errorf("Error creating Discovery REST Client: %v", err)
 	}
diff --git a/docs/advanced.md b/docs/advanced.md
index 9916465..46a06f3 100644
--- a/docs/advanced.md
+++ b/docs/advanced.md
@@ -197,9 +197,9 @@ The `target-versions` field in this custom file will set the default target vers
 
 Please note that we do not allow overriding anything contained in the default `versions.yaml` that Pluto uses.
 
-## Kube Context
+## Kube Context or kubeconfig
 
-When doing helm detection, you may want to use the `--kube-context` to specify a particular context you wish to use in your kubeconfig.
+When doing helm or apiVersion detection, you may want to use the `--kube-context` or `--kubeconfig` flags to specify a particular context, or a specific file path, that you wish to use for your kubeconfig.
 
 ## Environment Variables
 
diff --git a/pkg/discovery-api/discovery_api.go b/pkg/discovery-api/discovery_api.go
index d574835..46b656f 100644
--- a/pkg/discovery-api/discovery_api.go
+++ b/pkg/discovery-api/discovery_api.go
@@ -54,13 +54,13 @@ type DiscoveryClient struct {
 }
 
 // NewDiscoveryClient returns a new struct with config portions complete.
-func NewDiscoveryClient(namespace string, kubeContext string, instance *api.Instance) (*DiscoveryClient, error) {
+func NewDiscoveryClient(namespace string, kubeContext string, instance *api.Instance, kubeConfigPath string) (*DiscoveryClient, error) {
 	cl := &DiscoveryClient{
 		Instance: instance,
 	}
 
 	var err error
-	cl.restConfig, err = kube.GetConfig(kubeContext)
+	cl.restConfig, err = kube.GetConfig(kubeContext, kubeConfigPath)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/helm/helm.go b/pkg/helm/helm.go
index c044417..80f3025 100644
--- a/pkg/helm/helm.go
+++ b/pkg/helm/helm.go
@@ -74,8 +74,8 @@ type ChartMeta struct {
 }
 
 // NewHelm returns a basic helm struct with the version of helm requested
-func NewHelm(namespace string, kubeContext string, instance *api.Instance) (*Helm, error) {
-	config, err := kube.GetConfigInstance(kubeContext)
+func NewHelm(namespace string, kubeContext string, instance *api.Instance, kubeConfigPath string) (*Helm, error) {
+	config, err := kube.GetConfigInstance(kubeContext, kubeConfigPath)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/kube/kube.go b/pkg/kube/kube.go
index e184bf8..902fc05 100644
--- a/pkg/kube/kube.go
+++ b/pkg/kube/kube.go
@@ -29,6 +29,7 @@
 package kube
 
 import (
+	"flag"
 	"sync"
 
 	"k8s.io/client-go/kubernetes"
@@ -48,15 +49,15 @@ var kubeClient *Kube
 var once sync.Once
 
 // GetConfigInstance returns a Pluto Kubernetes interface based on the current configuration
-func GetConfigInstance(kubeContext string) (*Kube, error) {
+func GetConfigInstance(kubeContext string, kubeConfigPath string) (*Kube, error) {
 	var err error
 	var client kubernetes.Interface
 	var kubeConfig *rest.Config
 
-	kubeConfig, err = GetConfig(kubeContext)
-        if err != nil {
-                return nil, err
-        }
+	kubeConfig, err = GetConfig(kubeContext, kubeConfigPath)
+	if err != nil {
+		return nil, err
+	}
 
 	once.Do(func() {
 		if kubeClient == nil {
@@ -74,15 +75,21 @@ func GetConfigInstance(kubeContext string) (*Kube, error) {
 }
 
 // GetConfig returns the current kube config with a specific context
-func GetConfig(kubeContext string) (*rest.Config, error) {
+func GetConfig(kubeContext string, kubeConfigPath string) (*rest.Config, error) {
+
 	if kubeContext != "" {
 		klog.V(3).Infof("using kube context: %s", kubeContext)
 	}
 
+	fs := flag.NewFlagSet("fs", flag.ContinueOnError)
+	fs.String("kubeconfig", kubeConfigPath, "")
+	config.RegisterFlags(fs)
+
 	kubeConfig, err := config.GetConfigWithContext(kubeContext)
 	if err != nil {
 		return nil, err
 	}
+
 	return kubeConfig, nil
 }
 


Test patch:
diff --git a/pkg/kube/kube_test.go b/pkg/kube/kube_test.go
index 7acc514..9cbb42f 100644
--- a/pkg/kube/kube_test.go
+++ b/pkg/kube/kube_test.go
@@ -24,34 +24,59 @@ import (
 
 func Test_getKubeClient(t *testing.T) {
 	tests := []struct {
-		name        string
-		kubeContext string
-		kubeConfig  string
-		wantErr     bool
+		name           string
+		kubeContext    string
+		kubeConfig     string
+		kubeConfigPath string
+		wantErr        bool
 	}{
 		{
-			name:        "context does not exist",
-			kubeContext: "farglebargle",
-			kubeConfig:  "testdata/kubeconfig",
-			wantErr:     true,
+			name:           "context does not exist",
+			kubeContext:    "farglebargle",
+			kubeConfig:     "testdata/kubeconfig",
+			kubeConfigPath: "",
+			wantErr:        true,
 		},
 		{
-			name:        "context exists",
-			kubeContext: "kind-kind",
-			kubeConfig:  "testdata/kubeconfig",
-			wantErr:     false,
+			name:           "context exists",
+			kubeContext:    "kind-kind",
+			kubeConfig:     "testdata/kubeconfig",
+			kubeConfigPath: "",
+			wantErr:        false,
 		},
 		{
-			name:        "invalid kubeconfig",
-			kubeContext: "kind-kind",
-			kubeConfig:  "testdata/kubeconfig_invalid",
-			wantErr:     true,
+			name:           "invalid kubeconfig",
+			kubeContext:    "kind-kind",
+			kubeConfig:     "testdata/kubeconfig_invalid",
+			kubeConfigPath: "",
+			wantErr:        true,
+		},
+		{
+			name:           "invalid kubeconfig",
+			kubeContext:    "kind-kind",
+			kubeConfig:     "testdata/kubeconfig_invalid",
+			kubeConfigPath: "",
+			wantErr:        true,
+		},
+		{
+			name:           "valid kubeconfig path",
+			kubeContext:    "kind-kind",
+			kubeConfig:     "testdata/kubeconfig_invalid",
+			kubeConfigPath: "testdata/kubeconfig",
+			wantErr:        false,
+		},
+		{
+			name:           "invalid kubeconfig path",
+			kubeContext:    "kind-kind",
+			kubeConfig:     "testdata/kubeconfig",
+			kubeConfigPath: "testdata/kubeconfig_invalid",
+			wantErr:        true,
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
 			os.Setenv("KUBECONFIG", tt.kubeConfig)
-                        _, err := GetConfig(tt.kubeContext)
+			_, err := GetConfig(tt.kubeContext, tt.kubeConfigPath)
 			if tt.wantErr {
 				assert.Error(t, err)
 			} else {