Repository: helm/helm (go)

Issue:
helm lint is too noisy
When running `helm lint` against a large number of charts, I see a lot of output even in the case when no problems are detected.

Each invocation prints `1 chart(s) linted, 0 chart(s) failed` and `Using values from 'foo/bar/baz.yaml'`. This makes it hard to tell when there actually is a problem.

It would be nice if `helm lint` had a "quiet" option or some other way to reduce the verbosity, such that the only text that is written to the console is lint warnings/errors, and a clean run with no issues presents no output.



Pull request description:
feat(*): add --quiet flag to 'helm lint'
**What this PR does / why we need it**:

Quiet flag reduce the verbosity for `helm lint` command, such that the only text
that is written to the console is lint warnings/errors

Closes #9994

**Special notes for your reviewer**:

**If applicable**:
- [ ] this PR contains documentation
- [x] this PR contains unit tests
- [ ] this PR has been tested for backwards compatibility


Patch:
diff --git a/cmd/helm/lint.go b/cmd/helm/lint.go
index a7aac172a..73a37b6fe 100644
--- a/cmd/helm/lint.go
+++ b/cmd/helm/lint.go
@@ -29,6 +29,7 @@ import (
 	"helm.sh/helm/v3/pkg/action"
 	"helm.sh/helm/v3/pkg/cli/values"
 	"helm.sh/helm/v3/pkg/getter"
+	"helm.sh/helm/v3/pkg/lint/support"
 )
 
 var longLintHelp = `
@@ -76,12 +77,23 @@ func newLintCmd(out io.Writer) *cobra.Command {
 
 			var message strings.Builder
 			failed := 0
+			errorsOrWarnings := 0
 
 			for _, path := range paths {
-				fmt.Fprintf(&message, "==> Linting %s\n", path)
-
 				result := client.Run([]string{path}, vals)
 
+				// If there is no errors/warnings and quiet flag is set
+				// go to the next chart
+				hasWarningsOrErrors := action.HasWarningsOrErrors(result)
+				if hasWarningsOrErrors {
+					errorsOrWarnings++
+				}
+				if client.Quiet && !hasWarningsOrErrors {
+					continue
+				}
+
+				fmt.Fprintf(&message, "==> Linting %s\n", path)
+
 				// All the Errors that are generated by a chart
 				// that failed a lint will be included in the
 				// results.Messages so we only need to print
@@ -93,7 +105,9 @@ func newLintCmd(out io.Writer) *cobra.Command {
 				}
 
 				for _, msg := range result.Messages {
-					fmt.Fprintf(&message, "%s\n", msg)
+					if !client.Quiet || msg.Severity > support.InfoSev {
+						fmt.Fprintf(&message, "%s\n", msg)
+					}
 				}
 
 				if len(result.Errors) != 0 {
@@ -112,7 +126,9 @@ func newLintCmd(out io.Writer) *cobra.Command {
 			if failed > 0 {
 				return errors.New(summary)
 			}
-			fmt.Fprintln(out, summary)
+			if !client.Quiet || errorsOrWarnings > 0 {
+				fmt.Fprintln(out, summary)
+			}
 			return nil
 		},
 	}
@@ -120,6 +136,7 @@ func newLintCmd(out io.Writer) *cobra.Command {
 	f := cmd.Flags()
 	f.BoolVar(&client.Strict, "strict", false, "fail on lint warnings")
 	f.BoolVar(&client.WithSubcharts, "with-subcharts", false, "lint dependent charts")
+	f.BoolVar(&client.Quiet, "quiet", false, "print only warnings and errors")
 	addValueOptionsFlags(f, valueOpts)
 
 	return cmd
diff --git a/cmd/helm/push.go b/cmd/helm/push.go
index d2cf2693e..ed5d1699a 100644
--- a/cmd/helm/push.go
+++ b/cmd/helm/push.go
@@ -24,7 +24,6 @@ import (
 
 	"helm.sh/helm/v3/cmd/helm/require"
 	"helm.sh/helm/v3/pkg/action"
-	"helm.sh/helm/v3/pkg/pusher"
 )
 
 const pushDesc = `
@@ -42,23 +41,6 @@ func newPushCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 		Short: "push a chart to remote",
 		Long:  pushDesc,
 		Args:  require.MinimumNArgs(2),
-		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
-			if len(args) == 0 {
-				// Do file completion for the chart file to push
-				return nil, cobra.ShellCompDirectiveDefault
-			}
-			if len(args) == 1 {
-				providers := []pusher.Provider(pusher.All(settings))
-				var comps []string
-				for _, p := range providers {
-					for _, scheme := range p.Schemes {
-						comps = append(comps, fmt.Sprintf("%s://", scheme))
-					}
-				}
-				return comps, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace
-			}
-			return nil, cobra.ShellCompDirectiveNoFileComp
-		},
 		RunE: func(cmd *cobra.Command, args []string) error {
 			chartRef := args[0]
 			remote := args[1]
diff --git a/cmd/helm/registry_login.go b/cmd/helm/registry_login.go
index 8abf91e77..84f9f27b5 100644
--- a/cmd/helm/registry_login.go
+++ b/cmd/helm/registry_login.go
@@ -41,11 +41,10 @@ func newRegistryLoginCmd(cfg *action.Configuration, out io.Writer) *cobra.Comman
 	var passwordFromStdinOpt, insecureOpt bool
 
 	cmd := &cobra.Command{
-		Use:               "login [host]",
-		Short:             "login to a registry",
-		Long:              registryLoginDesc,
-		Args:              require.MinimumNArgs(1),
-		ValidArgsFunction: noCompletions,
+		Use:   "login [host]",
+		Short: "login to a registry",
+		Long:  registryLoginDesc,
+		Args:  require.MinimumNArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			hostname := args[0]
 
diff --git a/cmd/helm/registry_logout.go b/cmd/helm/registry_logout.go
index 0084f8c09..8d6a54d97 100644
--- a/cmd/helm/registry_logout.go
+++ b/cmd/helm/registry_logout.go
@@ -31,11 +31,10 @@ Remove credentials stored for a remote registry.
 
 func newRegistryLogoutCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 	return &cobra.Command{
-		Use:               "logout [host]",
-		Short:             "logout from a registry",
-		Long:              registryLogoutDesc,
-		Args:              require.MinimumNArgs(1),
-		ValidArgsFunction: noCompletions,
+		Use:   "logout [host]",
+		Short: "logout from a registry",
+		Long:  registryLogoutDesc,
+		Args:  require.MinimumNArgs(1),
 		RunE: func(cmd *cobra.Command, args []string) error {
 			hostname := args[0]
 			return action.NewRegistryLogout(cfg).Run(out, hostname)
diff --git a/cmd/helm/search_repo.go b/cmd/helm/search_repo.go
index f794f6bca..34232fcfa 100644
--- a/cmd/helm/search_repo.go
+++ b/cmd/helm/search_repo.go
@@ -326,7 +326,7 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell
 	cobra.CompDebugln(fmt.Sprintf("Completions after repos: %v", completions), settings.Debug)
 
 	// Now handle completions for url prefixes
-	for _, url := range []string{"oci://\tChart OCI prefix", "https://\tChart URL prefix", "http://\tChart URL prefix", "file://\tChart local URL prefix"} {
+	for _, url := range []string{"https://\tChart URL prefix", "http://\tChart URL prefix", "file://\tChart local URL prefix"} {
 		if strings.HasPrefix(toComplete, url) {
 			// The user already put in the full url prefix; we don't have
 			// anything to add, but make sure the shell does not default
diff --git a/cmd/helm/testdata/output/lint-quiet.txt b/cmd/helm/testdata/output/lint-quiet.txt
new file mode 100644
index 000000000..e69de29bb
diff --git a/pkg/action/lint.go b/pkg/action/lint.go
index bdb93dcc2..5b566e9d3 100644
--- a/pkg/action/lint.go
+++ b/pkg/action/lint.go
@@ -36,6 +36,7 @@ type Lint struct {
 	Strict        bool
 	Namespace     string
 	WithSubcharts bool
+	Quiet         bool
 }
 
 // LintResult is the result of Lint
@@ -75,6 +76,16 @@ func (l *Lint) Run(paths []string, vals map[string]interface{}) *LintResult {
 	return result
 }
 
+// HasWaringsOrErrors checks is LintResult has any warnings or errors
+func HasWarningsOrErrors(result *LintResult) bool {
+	for _, msg := range result.Messages {
+		if msg.Severity > support.InfoSev {
+			return true
+		}
+	}
+	return false
+}
+
 func lintChart(path string, vals map[string]interface{}, namespace string, strict bool) (support.Linter, error) {
 	var chartPath string
 	linter := support.Linter{}


Test patch:
diff --git a/cmd/helm/lint_test.go b/cmd/helm/lint_test.go
index 3501ccf87..ebea09bf0 100644
--- a/cmd/helm/lint_test.go
+++ b/cmd/helm/lint_test.go
@@ -37,6 +37,27 @@ func TestLintCmdWithSubchartsFlag(t *testing.T) {
 	runTestCmd(t, tests)
 }
 
+func TestLintCmdWithQuietFlag(t *testing.T) {
+	testChart1 := "testdata/testcharts/alpine"
+	testChart2 := "testdata/testcharts/chart-bad-requirements"
+	tests := []cmdTestCase{{
+		name:   "lint good chart using --quiet flag",
+		cmd:    fmt.Sprintf("lint --quiet %s", testChart1),
+		golden: "output/lint-quiet.txt",
+	}, {
+		name:      "lint two charts, one with error using --quiet flag",
+		cmd:       fmt.Sprintf("lint --quiet %s %s", testChart1, testChart2),
+		golden:    "output/lint-quiet-with-error.txt",
+		wantError: true,
+	}, {
+		name:   "lint chart with warning using --quiet flag",
+		cmd:    "lint --quiet testdata/testcharts/chart-with-only-crds",
+		golden: "output/lint-quiet-with-warning.txt",
+	}}
+	runTestCmd(t, tests)
+
+}
+
 func TestLintFileCompletion(t *testing.T) {
 	checkFileCompletion(t, "lint", true)
 	checkFileCompletion(t, "lint mypath", true) // Multiple paths can be given
diff --git a/cmd/helm/push_test.go b/cmd/helm/push_test.go
deleted file mode 100644
index 8e56d99dc..000000000
--- a/cmd/helm/push_test.go
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
-Copyright The Helm Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package main
-
-import (
-	"testing"
-)
-
-func TestPushFileCompletion(t *testing.T) {
-	checkFileCompletion(t, "push", true)
-	checkFileCompletion(t, "push package.tgz", false)
-	checkFileCompletion(t, "push package.tgz oci://localhost:5000", false)
-}
diff --git a/cmd/helm/registry_login_test.go b/cmd/helm/registry_login_test.go
deleted file mode 100644
index 517fe08e1..000000000
--- a/cmd/helm/registry_login_test.go
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
-Copyright The Helm Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package main
-
-import (
-	"testing"
-)
-
-func TestRegistryLoginFileCompletion(t *testing.T) {
-	checkFileCompletion(t, "registry login", false)
-}
diff --git a/cmd/helm/registry_logout_test.go b/cmd/helm/registry_logout_test.go
deleted file mode 100644
index 31f716725..000000000
--- a/cmd/helm/registry_logout_test.go
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
-Copyright The Helm Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package main
-
-import (
-	"testing"
-)
-
-func TestRegistryLogoutFileCompletion(t *testing.T) {
-	checkFileCompletion(t, "registry logout", false)
-}
diff --git a/cmd/helm/testdata/output/lint-quiet-with-error.txt b/cmd/helm/testdata/output/lint-quiet-with-error.txt
new file mode 100644
index 000000000..a4e8575f8
--- /dev/null
+++ b/cmd/helm/testdata/output/lint-quiet-with-error.txt
@@ -0,0 +1,8 @@
+==> Linting testdata/testcharts/chart-bad-requirements
+[ERROR] Chart.yaml: unable to parse YAML
+	error converting YAML to JSON: yaml: line 6: did not find expected '-' indicator
+[WARNING] templates/: directory not found
+[ERROR] : unable to load chart
+	cannot load Chart.yaml: error converting YAML to JSON: yaml: line 6: did not find expected '-' indicator
+
+Error: 2 chart(s) linted, 1 chart(s) failed
diff --git a/cmd/helm/testdata/output/lint-quiet-with-warning.txt b/cmd/helm/testdata/output/lint-quiet-with-warning.txt
new file mode 100644
index 000000000..02c6fa592
--- /dev/null
+++ b/cmd/helm/testdata/output/lint-quiet-with-warning.txt
@@ -0,0 +1,4 @@
+==> Linting testdata/testcharts/chart-with-only-crds
+[WARNING] templates/: directory not found
+
+1 chart(s) linted, 0 chart(s) failed