Repository: doitintl/kube-no-trouble
Pull request #194 (2021-07-20 12:52:47)

feat: Add --context (-x) option to select Kube context
This PR adds support to configure Kubernetes context via `--context (-x)` flag.

Addresses second part of #123 


Patch:
diff --git a/README.md b/README.md
index ddab691..b156513 100644
--- a/README.md
+++ b/README.md
@@ -78,20 +78,28 @@ $./kubent -h
 Usage of ./kubent:
   -a, --additional-kind strings   additional kinds of resources to report in Kind.version.group.com format
   -c, --cluster                   enable Cluster collector (default true)
+  -x, --context string            kubeconfig context
   -e, --exit-error                exit with non-zero code when issues are found
   -f, --filename strings          manifests to check, use - for stdin
       --helm2                     enable Helm v2 collector (default true)
       --helm3                     enable Helm v3 collector (default true)
-  -k, --kubeconfig string         path to the kubeconfig file (default "/Users/stepan/.kube/config")
+  -k, --kubeconfig string         path to the kubeconfig file
   -l, --log-level string          set log level (trace, debug, info, warn, error, fatal, panic, disabled) (default "info")
   -o, --output string             output format - [text|json] (default "text")
-  -t, --target-version string     target K8s version in major.minor format (autodetected by default)
+  -t, --target-version string     target K8s version in SemVer format (autodetected by default)
 ```
 
 - *`-a, --additional-kind`*
   Tells `kubent` to flag additional custom resources when found in the specified version. The flag can be used multiple
   times. The expected format is full *Kind.version.group.com* form - e.g. `-a ManagedCertificate.v1.networking.gke.io`.
 
+- *`-x, --context`*
+  Select context from kubeconfig file (`current-context` from the file is used by default).
+
+- *`k, --kubeconfig`*
+  Path to kubeconfig file to use. This takes precedence over `KUBECONFIG` environemnt variable, which is also supported
+  and can contain multiple paths, and default `~.kube/config`.
+
 - *`-t, --target-version`*
   `Kubent` will try to detect K8S cluster version and display only relevant findings. This flag allows to override this
   version for scenarios like use in CI with the file collector only, when detection from an actual cluster is not possible.
diff --git a/cmd/kubent/main.go b/cmd/kubent/main.go
index ee37875..560a31f 100644
--- a/cmd/kubent/main.go
+++ b/cmd/kubent/main.go
@@ -55,17 +55,17 @@ func storeCollector(collector collector.Collector, err error, collectors []colle
 func initCollectors(config *config.Config) []collector.Collector {
 	collectors := []collector.Collector{}
 	if config.Cluster {
-		collector, err := collector.NewClusterCollector(&collector.ClusterOpts{Kubeconfig: config.Kubeconfig}, config.AdditionalKinds)
+		collector, err := collector.NewClusterCollector(&collector.ClusterOpts{Kubeconfig: config.Kubeconfig, KubeContext: config.Context}, config.AdditionalKinds)
 		collectors = storeCollector(collector, err, collectors)
 	}
 
 	if config.Helm2 {
-		collector, err := collector.NewHelmV2Collector(&collector.HelmV2Opts{Kubeconfig: config.Kubeconfig})
+		collector, err := collector.NewHelmV2Collector(&collector.HelmV2Opts{Kubeconfig: config.Kubeconfig, KubeContext: config.Context})
 		collectors = storeCollector(collector, err, collectors)
 	}
 
 	if config.Helm3 {
-		collector, err := collector.NewHelmV3Collector(&collector.HelmV3Opts{Kubeconfig: config.Kubeconfig})
+		collector, err := collector.NewHelmV3Collector(&collector.HelmV3Opts{Kubeconfig: config.Kubeconfig, KubeContext: config.Context})
 		collectors = storeCollector(collector, err, collectors)
 	}
 
diff --git a/fixtures/kube.config.context b/fixtures/kube.config.context
new file mode 100644
index 0000000..5d6effa
--- /dev/null
+++ b/fixtures/kube.config.context
@@ -0,0 +1,23 @@
+apiVersion: v1
+clusters:
+- cluster:
+    server: https://test-cluster
+  name: test-cluster
+- cluster:
+    server: https://1.2.3.4:8443
+  name: default
+contexts:
+- context:
+    cluster: test-cluster
+    user: test-user
+  name: test-context
+- context:
+    cluster: default
+    user: default
+  name: default
+users:
+- name: test-user
+- name: default
+current-context: default
+kind: Config
+preferences: {}
diff --git a/pkg/collector/cluster.go b/pkg/collector/cluster.go
index 0431eb9..7456bd2 100644
--- a/pkg/collector/cluster.go
+++ b/pkg/collector/cluster.go
@@ -20,12 +20,13 @@ type ClusterCollector struct {
 
 type ClusterOpts struct {
 	Kubeconfig      string
+	KubeContext     string
 	ClientSet       dynamic.Interface
 	DiscoveryClient discovery.DiscoveryInterface
 }
 
 func NewClusterCollector(opts *ClusterOpts, additionalKinds []string) (*ClusterCollector, error) {
-	kubeCollector, err := newKubeCollector(opts.Kubeconfig, opts.DiscoveryClient)
+	kubeCollector, err := newKubeCollector(opts.Kubeconfig, opts.KubeContext, opts.DiscoveryClient)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/collector/helm2.go b/pkg/collector/helm2.go
index 0543351..7ebc49a 100644
--- a/pkg/collector/helm2.go
+++ b/pkg/collector/helm2.go
@@ -20,12 +20,13 @@ type HelmV2Collector struct {
 
 type HelmV2Opts struct {
 	Kubeconfig      string
+	KubeContext     string
 	DiscoveryClient discovery.DiscoveryInterface
 }
 
 func NewHelmV2Collector(opts *HelmV2Opts) (*HelmV2Collector, error) {
 
-	kubeCollector, err := newKubeCollector(opts.Kubeconfig, opts.DiscoveryClient)
+	kubeCollector, err := newKubeCollector(opts.Kubeconfig, opts.KubeContext, opts.DiscoveryClient)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/collector/helm3.go b/pkg/collector/helm3.go
index df00179..c4274e6 100644
--- a/pkg/collector/helm3.go
+++ b/pkg/collector/helm3.go
@@ -20,11 +20,12 @@ type HelmV3Collector struct {
 
 type HelmV3Opts struct {
 	Kubeconfig      string
+	KubeContext     string
 	DiscoveryClient discovery.DiscoveryInterface
 }
 
 func NewHelmV3Collector(opts *HelmV3Opts) (*HelmV3Collector, error) {
-	kubeCollector, err := newKubeCollector(opts.Kubeconfig, opts.DiscoveryClient)
+	kubeCollector, err := newKubeCollector(opts.Kubeconfig, opts.KubeContext, opts.DiscoveryClient)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/collector/kube.go b/pkg/collector/kube.go
index ecd11f4..a24f8ac 100644
--- a/pkg/collector/kube.go
+++ b/pkg/collector/kube.go
@@ -14,7 +14,7 @@ type kubeCollector struct {
 	restConfig      *rest.Config
 }
 
-func newKubeCollector(kubeconfig string, discoveryClient discovery.DiscoveryInterface) (*kubeCollector, error) {
+func newKubeCollector(kubeconfig string, kubecontext string, discoveryClient discovery.DiscoveryInterface) (*kubeCollector, error) {
 	col := &kubeCollector{}
 
 	if discoveryClient == nil {
@@ -24,7 +24,13 @@ func newKubeCollector(kubeconfig string, discoveryClient discovery.DiscoveryInte
 		}
 
 		config, err := pathOptions.GetStartingConfig()
-		clientConfig := clientcmd.NewDefaultClientConfig(*config, &clientcmd.ConfigOverrides{})
+
+		configOverrides := clientcmd.ConfigOverrides{}
+		if kubecontext != "" {
+			configOverrides.CurrentContext = kubecontext
+		}
+
+		clientConfig := clientcmd.NewDefaultClientConfig(*config, &configOverrides)
 		col.restConfig, err = clientConfig.ClientConfig()
 		if err != nil {
 			return nil, err
diff --git a/pkg/config/config.go b/pkg/config/config.go
index db4061b..a3887a7 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -14,6 +14,7 @@ import (
 type Config struct {
 	AdditionalKinds []string
 	Cluster         bool
+	Context         string
 	ExitError       bool
 	Filenames       []string
 	Helm2           bool
@@ -32,6 +33,7 @@ func NewFromFlags() (*Config, error) {
 
 	flag.StringSliceVarP(&config.AdditionalKinds, "additional-kind", "a", []string{}, "additional kinds of resources to report in Kind.version.group.com format")
 	flag.BoolVarP(&config.Cluster, "cluster", "c", true, "enable Cluster collector")
+	flag.StringVarP(&config.Context, "context", "x", "", "kubeconfig context")
 	flag.BoolVarP(&config.ExitError, "exit-error", "e", false, "exit with non-zero code when issues are found")
 	flag.BoolVar(&config.Helm2, "helm2", true, "enable Helm v2 collector")
 	flag.BoolVar(&config.Helm3, "helm3", true, "enable Helm v3 collector")


Test patch:
diff --git a/pkg/collector/kube_test.go b/pkg/collector/kube_test.go
index a6b719b..3c6f8f8 100644
--- a/pkg/collector/kube_test.go
+++ b/pkg/collector/kube_test.go
@@ -12,7 +12,7 @@ import (
 
 func TestNewKubeCollector(t *testing.T) {
 	clientSet := fake.NewSimpleClientset()
-	col, err := newKubeCollector("", clientSet.Discovery())
+	col, err := newKubeCollector("", "", clientSet.Discovery())
 
 	if err != nil {
 		t.Errorf("Failed to create kubeCollector from fake discovery client")
@@ -23,7 +23,7 @@ func TestNewKubeCollector(t *testing.T) {
 }
 
 func TestNewKubeCollectorError(t *testing.T) {
-	_, err := newKubeCollector("does-not-exist", nil)
+	_, err := newKubeCollector("does-not-exist", "", nil)
 
 	if err == nil {
 		t.Errorf("Expected to fail with non-existent kubeconfig")
@@ -31,7 +31,7 @@ func TestNewKubeCollectorError(t *testing.T) {
 }
 
 func TestNewKubeCollectorWithKubeconfigPath(t *testing.T) {
-	_, err := newKubeCollector("../../fixtures/kube.config.basic", nil)
+	_, err := newKubeCollector("../../fixtures/kube.config.basic", "", nil)
 	if err != nil {
 		t.Fatalf("Failed with: %s", err)
 	}
@@ -45,7 +45,7 @@ func TestNewKubeCollectorMultipleFiles(t *testing.T) {
 		t.Fatalf("Failed so set %s env variable for test: %s", kcEnvVar, err)
 	}
 
-	if _, err := newKubeCollector("", nil); err != nil {
+	if _, err := newKubeCollector("", "", nil); err != nil {
 		t.Fatalf("Failed with: %s", err)
 	}
 
@@ -69,7 +69,7 @@ func TestGetServerVersion(t *testing.T) {
 		GitVersion: gitVersion,
 	}
 
-	collector, err := newKubeCollector("", clientSet.Discovery())
+	collector, err := newKubeCollector("", "", clientSet.Discovery())
 	if err != nil {
 		t.Fatalf("failed to create kubeCollector from fake client: %s", err)
 	}
@@ -83,3 +83,27 @@ func TestGetServerVersion(t *testing.T) {
 		t.Errorf("Expected version: %s, instead got: %s", expectedVersion.String(), version.String())
 	}
 }
+
+func TestContext(t *testing.T) {
+	expectedContext := "test-context"
+	expectedHost := "https://test-cluster"
+
+	collector, err := newKubeCollector("../../fixtures/kube.config.context", expectedContext, nil)
+	if err != nil {
+		t.Fatalf("Failed to create kubeCollector from fake client with context %s: %s", expectedContext, err)
+	}
+
+	host := collector.GetRestConfig().Host
+	if host != expectedHost {
+		t.Fatalf("Expected host from context %s to be: %s, got %s instead", expectedContext, expectedHost, host)
+	}
+}
+
+func TestContextMissing(t *testing.T) {
+	expectedContext := "non-existent"
+
+	_, err := newKubeCollector("../../fixtures/kube.config.context", expectedContext, nil)
+	if err == nil {
+		t.Fatalf("Expected to fail when uisng non-existent context: %s", expectedContext)
+	}
+}
diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
index 0505fbc..2d3e6fd 100644
--- a/pkg/config/config_test.go
+++ b/pkg/config/config_test.go
@@ -136,3 +136,28 @@ func TestTargetVersionInvalid(t *testing.T) {
 		}
 	}
 }
+
+func TestContext(t *testing.T) {
+	validContexts := []string{
+		"my-context",
+	}
+
+	oldArgs := os.Args[1]
+	defer func() { os.Args[1] = oldArgs }()
+
+	for _, context := range validContexts {
+		// reset for testing
+		pflag.CommandLine = pflag.NewFlagSet(os.Args[0], pflag.ExitOnError)
+
+		os.Args[1] = "--context=" + context
+		config, err := NewFromFlags()
+
+		if err != nil {
+			t.Errorf("Flags parsing failed %s", err)
+		}
+
+		if config.Context != context {
+			t.Fatalf("Context not parsed correctly: expected: %s, got: %s", context, config.Context)
+		}
+	}
+}