From 9db231d4c5beebadd112ddd1de03ee73615c4b3b Mon Sep 17 00:00:00 2001 From: Baiju Muthukadan Date: Sat, 29 May 2021 20:26:22 +0530 Subject: [PATCH 1/2] Add flag to exit with 1 if no results found The `--fail-if-no-results-found` flag exit with status code 1 if no search results found during `helm search repo` and `helm search hub` commands. This is useful for automation scripts. closes #7197 Signed-off-by: Baiju Muthukadan --- cmd/helm/search_hub.go | 20 +++++++++++++------- cmd/helm/search_repo.go | 28 +++++++++++++++++----------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/cmd/helm/search_hub.go b/cmd/helm/search_hub.go index b8887efd5..e16d88056 100644 --- a/cmd/helm/search_hub.go +++ b/cmd/helm/search_hub.go @@ -19,6 +19,7 @@ package main import ( "fmt" "io" + "os" "strings" "github.com/gosuri/uitable" @@ -54,6 +55,7 @@ type searchHubOptions struct { maxColWidth uint outputFormat output.Format listRepoURL bool + noResultsFail bool } func newSearchHubCmd(out io.Writer) *cobra.Command { @@ -72,7 +74,7 @@ func newSearchHubCmd(out io.Writer) *cobra.Command { f.StringVar(&o.searchEndpoint, "endpoint", "https://hub.helm.sh", "Hub instance to query for charts") f.UintVar(&o.maxColWidth, "max-col-width", 50, "maximum column width for output table") f.BoolVar(&o.listRepoURL, "list-repo-url", false, "print charts repository URL") - + f.BoolVarP(&o.noResultsFail, "fail-if-no-results-found", "f", false, "exit with status code 1 if no results found") bindOutputFlag(cmd, &o.outputFormat) return cmd @@ -91,7 +93,7 @@ func (o *searchHubOptions) run(out io.Writer, args []string) error { return fmt.Errorf("unable to perform search against %q", o.searchEndpoint) } - return o.outputFormat.Write(out, newHubSearchWriter(results, o.searchEndpoint, o.maxColWidth, o.listRepoURL)) + return o.outputFormat.Write(out, newHubSearchWriter(results, o.searchEndpoint, o.maxColWidth, o.noResultsFail, o.listRepoURL)) } type hubChartRepo struct { @@ -108,12 +110,13 @@ type hubChartElement struct { } type hubSearchWriter struct { - elements []hubChartElement - columnWidth uint - listRepoURL bool + elements []hubChartElement + columnWidth uint + listRepoURL bool + noResultsFail bool } -func newHubSearchWriter(results []monocular.SearchResult, endpoint string, columnWidth uint, listRepoURL bool) *hubSearchWriter { +func newHubSearchWriter(results []monocular.SearchResult, endpoint string, columnWidth uint, listRepoURL, noResultsFail bool) *hubSearchWriter { var elements []hubChartElement for _, r := range results { // Backwards compatibility for Monocular @@ -126,7 +129,7 @@ func newHubSearchWriter(results []monocular.SearchResult, endpoint string, colum elements = append(elements, hubChartElement{url, r.Relationships.LatestChartVersion.Data.Version, r.Relationships.LatestChartVersion.Data.AppVersion, r.Attributes.Description, hubChartRepo{URL: r.Attributes.Repo.URL, Name: r.Attributes.Repo.Name}}) } - return &hubSearchWriter{elements, columnWidth, listRepoURL} + return &hubSearchWriter{elements, columnWidth, listRepoURL, noResultsFail} } func (h *hubSearchWriter) WriteTable(out io.Writer) error { @@ -135,6 +138,9 @@ func (h *hubSearchWriter) WriteTable(out io.Writer) error { if err != nil { return fmt.Errorf("unable to write results: %s", err) } + if h.noResultsFail { + os.Exit(1) + } return nil } table := uitable.New() diff --git a/cmd/helm/search_repo.go b/cmd/helm/search_repo.go index 34232fcfa..7ae3fcaaf 100644 --- a/cmd/helm/search_repo.go +++ b/cmd/helm/search_repo.go @@ -64,14 +64,15 @@ Repositories are managed with 'helm repo' commands. const searchMaxScore = 25 type searchRepoOptions struct { - versions bool - regexp bool - devel bool - version string - maxColWidth uint - repoFile string - repoCacheDir string - outputFormat output.Format + versions bool + regexp bool + devel bool + version string + maxColWidth uint + repoFile string + repoCacheDir string + outputFormat output.Format + noResultsFail bool } func newSearchRepoCmd(out io.Writer) *cobra.Command { @@ -94,6 +95,7 @@ func newSearchRepoCmd(out io.Writer) *cobra.Command { f.BoolVar(&o.devel, "devel", false, "use development versions (alpha, beta, and release candidate releases), too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored") f.StringVar(&o.version, "version", "", "search using semantic versioning constraints on repositories you have added") f.UintVar(&o.maxColWidth, "max-col-width", 50, "maximum column width for output table") + f.BoolVarP(&o.noResultsFail, "fail-if-no-results-found", "f", false, "exit with status code 1 if no results found") bindOutputFlag(cmd, &o.outputFormat) return cmd @@ -124,7 +126,7 @@ func (o *searchRepoOptions) run(out io.Writer, args []string) error { return err } - return o.outputFormat.Write(out, &repoSearchWriter{data, o.maxColWidth}) + return o.outputFormat.Write(out, &repoSearchWriter{data, o.maxColWidth, o.noResultsFail}) } func (o *searchRepoOptions) setupSearchedVersion() { @@ -205,8 +207,9 @@ type repoChartElement struct { } type repoSearchWriter struct { - results []*search.Result - columnWidth uint + results []*search.Result + columnWidth uint + noResultsFail bool } func (r *repoSearchWriter) WriteTable(out io.Writer) error { @@ -215,6 +218,9 @@ func (r *repoSearchWriter) WriteTable(out io.Writer) error { if err != nil { return fmt.Errorf("unable to write results: %s", err) } + if r.noResultsFail { + os.Exit(1) + } return nil } table := uitable.New() From 26ba3e06e77504bd6faaac42c74402f1b4987445 Mon Sep 17 00:00:00 2001 From: Baiju Muthukadan Date: Sat, 29 May 2021 21:56:37 +0530 Subject: [PATCH 2/2] test cases for exit status code (based on the `TestPluginExitCode` test) Signed-off-by: Baiju Muthukadan --- cmd/helm/search_hub_test.go | 58 ++++++++++++++++++++++++++++++++++++ cmd/helm/search_repo_test.go | 50 +++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/cmd/helm/search_hub_test.go b/cmd/helm/search_hub_test.go index 7df54ea8f..a571ee268 100644 --- a/cmd/helm/search_hub_test.go +++ b/cmd/helm/search_hub_test.go @@ -17,9 +17,12 @@ limitations under the License. package main import ( + "bytes" "fmt" "net/http" "net/http/httptest" + "os" + "os/exec" "testing" ) @@ -90,3 +93,58 @@ func TestSearchHubOutputCompletion(t *testing.T) { func TestSearchHubFileCompletion(t *testing.T) { checkFileCompletion(t, "search hub", true) // File completion may be useful when inputting a keyword } + +func TestSearchHubCmdExitCode(t *testing.T) { + + if os.Getenv("RUN_MAIN_FOR_TESTING") == "1" { + // Setup a mock search service + var searchResult = `{"data":[]}` + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, searchResult) + })) + defer ts.Close() + + os.Args = []string{"helm", "search", "hub", "syzygy", "--endpoint", ts.URL, "--fail-if-no-results-found"} + + // We DO call helm's main() here. So this looks like a normal `helm` process. + main() + + // As main calls os.Exit, we never reach this line. + // But the test called this block of code catches and verifies the exit code. + return + } + + // Do a second run of this specific test(TestPluginExitCode) with RUN_MAIN_FOR_TESTING=1 set, + // So that the second run is able to run main() and this first run can verify the exit status returned by that. + // + // This technique originates from https://talks.golang.org/2014/testing.slide#23. + cmd := exec.Command(os.Args[0], "-test.run=TestSearchHubCmdExitCode") + cmd.Env = append( + os.Environ(), + "RUN_MAIN_FOR_TESTING=1", + ) + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + cmd.Stdout = stdout + cmd.Stderr = stderr + err := cmd.Run() + exiterr, ok := err.(*exec.ExitError) + + if !ok { + t.Fatalf("Unexpected error returned by os.Exit: %T", err) + } + + expectedStdout := "No results found\n" + if stdout.String() != expectedStdout { + t.Errorf("Expected %q written to stdout: Got %q", expectedStdout, stdout.String()) + } + + if stderr.String() != "" { + t.Errorf("Expected no writes to stderr: Got %q", stderr.String()) + } + + if exiterr.ExitCode() != 1 { + t.Errorf("Expected exit code 1: Got %d", exiterr.ExitCode()) + } + +} diff --git a/cmd/helm/search_repo_test.go b/cmd/helm/search_repo_test.go index 58ba3a715..6d917fb49 100644 --- a/cmd/helm/search_repo_test.go +++ b/cmd/helm/search_repo_test.go @@ -17,6 +17,9 @@ limitations under the License. package main import ( + "bytes" + "os" + "os/exec" "testing" ) @@ -91,3 +94,50 @@ func TestSearchRepoOutputCompletion(t *testing.T) { func TestSearchRepoFileCompletion(t *testing.T) { checkFileCompletion(t, "search repo", true) // File completion may be useful when inputting a keyword } + +func TestSearchRepositoriesCmdExitCode(t *testing.T) { + if os.Getenv("RUN_MAIN_FOR_TESTING") == "1" { + os.Args = []string{"helm", "search", "repo", "syzygy", "--fail-if-no-results-found"} + + // We DO call helm's main() here. So this looks like a normal `helm` process. + main() + + // As main calls os.Exit, we never reach this line. + // But the test called this block of code catches and verifies the exit code. + return + } + + // Do a second run of this specific test(TestPluginExitCode) with RUN_MAIN_FOR_TESTING=1 set, + // So that the second run is able to run main() and this first run can verify the exit status returned by that. + // + // This technique originates from https://talks.golang.org/2014/testing.slide#23. + cmd := exec.Command(os.Args[0], "-test.run=TestSearchRepositoriesCmdExitCode") + cmd.Env = append( + os.Environ(), + "RUN_MAIN_FOR_TESTING=1", + ) + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + cmd.Stdout = stdout + cmd.Stderr = stderr + err := cmd.Run() + exiterr, ok := err.(*exec.ExitError) + + if !ok { + t.Fatalf("Unexpected error returned by os.Exit: %T", err) + } + + expectedStdout := "No results found\n" + if stdout.String() != expectedStdout { + t.Errorf("Expected %q written to stdout: Got %q", expectedStdout, stdout.String()) + } + + if stderr.String() != "" { + t.Errorf("Expected no writes to stderr: Got %q", stderr.String()) + } + + if exiterr.ExitCode() != 1 { + t.Errorf("Expected exit code 1: Got %d", exiterr.ExitCode()) + } + +}