Repository: doitintl/kube-no-trouble (go)

Issue:
Do not print additional warnings from go-client
After the latest upgrade of K8S libs. (#401) the stderr output contains warnings from the API client. While these are certainly useful in scenarios like `kubectl`, I think these are undesirable in our case and also don't follow the same logging format:

```shell
$./bin/kubent-darwin-amd64
4:32PM INF >>> Kube No Trouble `kubent` <<<
4:32PM INF version dev (git sha dev)
4:32PM INF Initializing collectors and retrieving data
W1124 16:32:06.765179   25068 gcp.go:119] WARNING: the gcp auth plugin is deprecated in v1.22+, unavailable in v1.26+; use gcloud instead.
To learn more, consult https://cloud.google.com/blog/products/containers-kubernetes/kubectl-auth-changes-in-gke
4:32PM INF Target K8s version is 1.23.8-gke.1900
W1124 16:32:11.190950   25068 warnings.go:70] policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+
W1124 16:32:15.931515   25068 warnings.go:70] policy/v1beta1 PodSecurityPolicy is deprecated in v1.21+, unavailable in v1.25+
4:32PM INF Retrieved 10 resources from collector name=Cluster
```

Pull request:
feat: Disable any logging from go-client's klog
This PR disables warnings from K8S go-client. Unfortunately, this is not sufficient to also suppress the warning about deprecated auth plugins, therefore the need for klog output code too.

But in general any direct logging from 3rd-party libs is undesirable, and this configures K8S's go-client klog to discard any logs.

I've added some tests to cover some unexpected 3rd-party output, but asthis depends on the environment and kubeconfig config, incl. providers, and is not easily reproducible, this should be covered by an integration test, as the current test does not fully cover this. Opened #406.

fixes #402


Compare output from #402 (pre) with the following (post) - the warnings from klog are gone:
```shell
$ ./bin/kubent-darwin-amd64
11:35AM INF >>> Kube No Trouble `kubent` <<<
11:35AM INF version dev (git sha dev)
11:35AM INF Initializing collectors and retrieving data
11:35AM INF Target K8s version is 1.23.8-gke.1900
11:35AM INF Retrieved 10 resources from collector name=Cluster
11:35AM INF Retrieved 40 resources from collector name="Helm v3"
11:35AM INF Loaded ruleset name=custom.rego.tmpl
11:35AM INF Loaded ruleset name=deprecated-1-16.rego
11:35AM INF Loaded ruleset name=deprecated-1-22.rego
11:35AM INF Loaded ruleset name=deprecated-1-25.rego
11:35AM INF Loaded ruleset name=deprecated-1-26.rego
11:35AM INF Loaded ruleset name=deprecated-future.rego
```

_The usual SonarCloud's warning is about some dupe code in tests - not a newly added code, and in tests._

Patch:
diff --git a/cmd/kubent/main.go b/cmd/kubent/main.go
index ef3fdd9..d9f93d7 100644
--- a/cmd/kubent/main.go
+++ b/cmd/kubent/main.go
@@ -1,7 +1,9 @@
 package main
 
 import (
+	"flag"
 	"fmt"
+	"io"
 	"os"
 
 	"github.com/doitintl/kube-no-trouble/pkg/collector"
@@ -16,6 +18,7 @@ import (
 	_ "k8s.io/client-go/plugin/pkg/client/auth/azure"
 	_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
 	_ "k8s.io/client-go/plugin/pkg/client/auth/oidc"
+	"k8s.io/klog/v2"
 )
 
 var (
@@ -95,7 +98,7 @@ func getServerVersion(cv *judge.Version, collectors []collector.Collector) (*jud
 func main() {
 	exitCode := EXIT_CODE_FAIL_GENERIC
 
-	log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
+	configureGlobalLogging()
 
 	config, err := config.NewFromFlags()
 	if err != nil {
@@ -163,6 +166,17 @@ func main() {
 	os.Exit(exitCode)
 }
 
+func configureGlobalLogging() {
+	// disable any logging from K8S go-client
+	// unfortunately restConfig.WarningHandler does not handle auth plugins
+	klog.SetOutput(io.Discard)
+	flags := &flag.FlagSet{}
+	klog.InitFlags(flags)
+	flags.Set("logtostderr", "false")
+
+	log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
+}
+
 func outputResults(results []judge.Result, outputType string, outputFile string) error {
 	printer, err := printer.NewPrinter(outputType, outputFile)
 	if err != nil {
diff --git a/go.mod b/go.mod
index 34adb5f..12eabe6 100644
--- a/go.mod
+++ b/go.mod
@@ -6,11 +6,12 @@ require (
 	github.com/ghodss/yaml v1.0.0
 	github.com/hashicorp/go-version v1.3.0
 	github.com/open-policy-agent/opa v0.46.1
-	github.com/rs/zerolog v1.21.1-0.20210413053206-582f0cf0e39b
+	github.com/rs/zerolog v1.26.1
 	github.com/spf13/pflag v1.0.5
 	helm.sh/helm/v3 v3.10.2
 	k8s.io/apimachinery v0.25.4
 	k8s.io/client-go v0.25.4
+	k8s.io/klog/v2 v2.70.1
 )
 
 require (
@@ -77,7 +78,6 @@ require (
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 	k8s.io/api v0.25.4 // indirect
 	k8s.io/apiextensions-apiserver v0.25.2 // indirect
-	k8s.io/klog/v2 v2.70.1 // indirect
 	k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect
 	k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect
 	sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
diff --git a/go.sum b/go.sum
index f43ba2e..9104af5 100644
--- a/go.sum
+++ b/go.sum
@@ -104,7 +104,6 @@ github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnht
 github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
 github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
 github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
-github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
 github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -393,9 +392,9 @@ github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqn
 github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
 github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
-github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
-github.com/rs/zerolog v1.21.1-0.20210413053206-582f0cf0e39b h1:lZjlZ6/ExLhIqPOmlh0dVJnBbiKokcjffr022PlkSSo=
-github.com/rs/zerolog v1.21.1-0.20210413053206-582f0cf0e39b/go.mod h1:ZPhntP/xmq1nnND05hhpAh2QMhSsA4UN3MGZ6O2J3hM=
+github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
+github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
+github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
 github.com/rubenv/sql-migrate v1.1.2 h1:9M6oj4e//owVVHYrFISmY9LBRw6gzkCNmD9MV36tZeQ=
 github.com/rubenv/sql-migrate v1.1.2/go.mod h1:/7TZymwxN8VWumcIxw1jjHEcR1djpdkMHQPT4FWdnbQ=
 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@@ -470,6 +469,7 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
 golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e h1:T8NU3HyQ8ClP4SEE+KbFlg6n0NhuTsN4MyznaarGsZM=
 golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
diff --git a/pkg/collector/kube.go b/pkg/collector/kube.go
index f6e102e..e4b3bc1 100644
--- a/pkg/collector/kube.go
+++ b/pkg/collector/kube.go
@@ -38,6 +38,7 @@ func newClientRestConfig(kubeconfig string, kubecontext string, inClusterFn func
 	if kubeconfig == "" {
 		if restConfig, err := inClusterFn(); err == nil {
 			restConfig.UserAgent = userAgent
+			restConfig.WarningHandler = rest.NoWarnings{}
 			return restConfig, nil
 		}
 	}
@@ -64,6 +65,7 @@ func newClientRestConfig(kubeconfig string, kubecontext string, inClusterFn func
 	}
 
 	restConfig.UserAgent = userAgent
+	restConfig.WarningHandler = rest.NoWarnings{}
 	return restConfig, nil
 }
 


Test patch:
diff --git a/cmd/kubent/main_test.go b/cmd/kubent/main_test.go
index 4491f97..80fc219 100644
--- a/cmd/kubent/main_test.go
+++ b/cmd/kubent/main_test.go
@@ -117,20 +117,22 @@ func TestMainExitCodes(t *testing.T) {
 		expected    int      // expected exit code
 		stdout      string   // expected stdout
 		outFileName string
+		emptyStderr bool
 	}{
-		{"success", []string{clusterFlagDisabled, helm3FlagDisabled}, 0, "", ""},
-		{"errorBadFlag", []string{"-c=not-boolean"}, 2, "", ""},
-		{"successFound", []string{"-o=json", clusterFlagDisabled, helm3FlagDisabled, "-f=" + filepath.Join(FIXTURES_DIR, "deployment-v1beta1.yaml")}, 0, string(expectedJsonOutput), ""},
-		{"exitErrorFlagNone", []string{clusterFlagDisabled, helm3FlagDisabled, "-e"}, 0, "", ""},
-		{"exitErrorFlagFound", []string{clusterFlagDisabled, helm3FlagDisabled, "-e", "-f=" + filepath.Join(FIXTURES_DIR, "deployment-v1beta1.yaml")}, 200, "", ""},
-		{"version short flag set", []string{"-v"}, 0, "", ""},
-		{"version long flag set", []string{"--version"}, 0, "", ""},
-		{"empty text output", []string{clusterFlagDisabled, helm3FlagDisabled}, 0, "", ""},
-		{"empty json output", []string{"-o=json", clusterFlagDisabled, helm3FlagDisabled}, 0, "[]\n", ""},
-		{"json-file", []string{"-o=json", clusterFlagDisabled, helm3FlagDisabled, "-f=" + filepath.Join(FIXTURES_DIR, "deployment-v1beta1.yaml")}, 0, "", filepath.Join(tmpDir, "json-file.out")},
-		{"text-file", []string{"-o=json", clusterFlagDisabled, helm3FlagDisabled, "-f=" + filepath.Join(FIXTURES_DIR, "deployment-v1beta1.yaml")}, 0, "", filepath.Join(tmpDir, "text-file.out")},
-		{"json-stdout", []string{"-o=json", clusterFlagDisabled, helm3FlagDisabled, "-f=" + filepath.Join(FIXTURES_DIR, "deployment-v1beta1.yaml")}, 0, string(expectedJsonOutput), "-"},
-		{"error-bad-file", []string{clusterFlagDisabled, helm3FlagDisabled}, 1, "", "/this/dir/is/unlikely/to/exist"},
+		{"success", []string{clusterFlagDisabled, helm3FlagDisabled}, 0, "", "", false},
+		{"errorBadFlag", []string{"-c=not-boolean"}, 2, "", "", false},
+		{"successFound", []string{"-o=json", clusterFlagDisabled, helm3FlagDisabled, "-f=" + filepath.Join(FIXTURES_DIR, "deployment-v1beta1.yaml")}, 0, string(expectedJsonOutput), "", false},
+		{"exitErrorFlagNone", []string{clusterFlagDisabled, helm3FlagDisabled, "-e"}, 0, "", "", false},
+		{"exitErrorFlagFound", []string{clusterFlagDisabled, helm3FlagDisabled, "-e", "-f=" + filepath.Join(FIXTURES_DIR, "deployment-v1beta1.yaml")}, 200, "", "", false},
+		{"version short flag set", []string{"-v"}, 0, "", "", false},
+		{"version long flag set", []string{"--version"}, 0, "", "", false},
+		{"empty text output", []string{clusterFlagDisabled, helm3FlagDisabled}, 0, "", "", false},
+		{"empty json output", []string{"-o=json", clusterFlagDisabled, helm3FlagDisabled}, 0, "[]\n", "", false},
+		{"json-file", []string{"-o=json", clusterFlagDisabled, helm3FlagDisabled, "-f=" + filepath.Join(FIXTURES_DIR, "deployment-v1beta1.yaml")}, 0, "", filepath.Join(tmpDir, "json-file.out"), false},
+		{"text-file", []string{"-o=text", clusterFlagDisabled, helm3FlagDisabled, "-f=" + filepath.Join(FIXTURES_DIR, "deployment-v1beta1.yaml")}, 0, "", filepath.Join(tmpDir, "text-file.out"), false},
+		{"json-stdout", []string{"-o=json", clusterFlagDisabled, helm3FlagDisabled, "-f=" + filepath.Join(FIXTURES_DIR, "deployment-v1beta1.yaml")}, 0, string(expectedJsonOutput), "-", false},
+		{"error-bad-file", []string{clusterFlagDisabled, helm3FlagDisabled}, 1, "", "/this/dir/is/unlikely/to/exist", false},
+		{"no-3rdparty-output", []string{clusterFlagDisabled, helm3FlagDisabled, "-l=disabled"}, 0, "", "", true},
 	}
 
 	if os.Getenv("TEST_EXIT_CODE") == "1" {
@@ -150,11 +152,18 @@ func TestMainExitCodes(t *testing.T) {
 			}
 			base64Args, _ := encodeBase64(tc.args)
 
+			var stdout, stderr bytes.Buffer
+
 			cmd := exec.Command(os.Args[0], "-test.run=TestMainExitCodes")
 			cmd.Env = append(os.Environ(),
 				"TEST_EXIT_CODE=1",
 				"TEST_ARGS="+base64Args)
-			out, err := cmd.Output()
+			cmd.Stdout = &stdout
+			cmd.Stderr = &stderr
+			err := cmd.Run()
+
+			outStr := stdout.String()
+			errStr := stderr.String()
 
 			if tc.expected == 0 && err != nil {
 				t.Fatalf("expected to succeed with exit code %d, failed with %v", tc.expected, err)
@@ -171,16 +180,19 @@ func TestMainExitCodes(t *testing.T) {
 					t.Fatalf("expected to get exit code %d, failed with %v", tc.expected, err)
 				}
 			}
-			if tc.expected == 0 && err == nil && tc.stdout != string(out) {
-				t.Fatalf("expected to get stdout as %s, instead got %s", tc.stdout, out)
+			if tc.expected == 0 && err == nil && tc.stdout != outStr {
+				t.Fatalf("expected to get stdout as %s, instead got %s", tc.stdout, outStr)
 			}
 
 			if tc.expected == 0 && err == nil && tc.outFileName != "" && tc.outFileName != "-" {
 				if fs, err := os.Stat(tc.outFileName); err != nil || fs.Size() == 0 {
-					t.Fatalf("expected non-empty outputdile: %s, got error: %v", tc.outFileName, err)
+					t.Fatalf("expected non-empty outputfile: %s, got error: %v", tc.outFileName, err)
 				}
 			}
 
+			if tc.emptyStderr && errStr != "" {
+				t.Fatalf("expected empty stderr, got: %s", errStr)
+			}
 		})
 	}
 }
@@ -282,3 +294,9 @@ func Test_outputResults(t *testing.T) {
 		})
 	}
 }
+
+func Test_configureGlobalLogging(t *testing.T) {
+	// just make sure the method runs, this is mostly covered
+	//by the Test_MainExitCodes
+	configureGlobalLogging()
+}
diff --git a/pkg/collector/kube_test.go b/pkg/collector/kube_test.go
index 3a2100f..26bacea 100644
--- a/pkg/collector/kube_test.go
+++ b/pkg/collector/kube_test.go
@@ -3,6 +3,7 @@ package collector
 import (
 	"os"
 	"path/filepath"
+	"reflect"
 	"testing"
 
 	"k8s.io/client-go/rest"
@@ -141,6 +142,14 @@ func TestNewClientRestConfigWithContext(t *testing.T) {
 	if config.Host != expectedHost {
 		t.Fatalf("Expected host from context %s to be: %s, got %s instead", expectedContext, expectedHost, config.Host)
 	}
+
+	if config.UserAgent != USER_AGENT {
+		t.Fatalf("Expected %s UserAgent, instead got: %s", USER_AGENT, config.UserAgent)
+	}
+
+	if _, ok := config.WarningHandler.(rest.NoWarnings); !ok {
+		t.Fatalf("Expected NoWarnings warnings handler, instead got: %s", reflect.TypeOf(config.WarningHandler).Name())
+	}
 }
 
 func TestNewClientRestConfigContextMissing(t *testing.T) {
@@ -165,6 +174,12 @@ func TestNewClientRestConfigInCluster(t *testing.T) {
 	if cfg.Host != expectedHost {
 		t.Fatalf("Expected %s host, instead got: %s", expectedHost, cfg.Host)
 	}
+	if cfg.UserAgent != USER_AGENT {
+		t.Fatalf("Expected %s UserAgent, instead got: %s", USER_AGENT, cfg.UserAgent)
+	}
+	if _, ok := cfg.WarningHandler.(rest.NoWarnings); !ok {
+		t.Fatalf("Expected NoWarnings warnings handler, instead got: %s", reflect.TypeOf(cfg.WarningHandler).Name())
+	}
 }
 
 type env struct {