diff --git a/cmd/krew/cmd/search.go b/cmd/krew/cmd/search.go index 8a8570c2..313f1c42 100644 --- a/cmd/krew/cmd/search.go +++ b/cmd/krew/cmd/search.go @@ -22,11 +22,12 @@ import ( "github.com/pkg/errors" "github.com/sahilm/fuzzy" "github.com/spf13/cobra" + "k8s.io/klog" + "sigs.k8s.io/krew/internal/index/indexoperations" "sigs.k8s.io/krew/internal/index/indexscanner" "sigs.k8s.io/krew/internal/installation" "sigs.k8s.io/krew/pkg/constants" - "sigs.k8s.io/krew/pkg/index" ) // searchCmd represents the search command @@ -43,58 +44,82 @@ Examples: To fuzzy search plugins with a keyword: kubectl krew search KEYWORD`, RunE: func(cmd *cobra.Command, args []string) error { - plugins, err := indexscanner.LoadPluginListFromFS(paths.IndexPluginsPath(constants.DefaultIndexName)) - if err != nil { - return errors.Wrap(err, "failed to load the list of plugins from the index") + indexes := []indexoperations.Index{ + { + Name: constants.DefaultIndexName, + URL: constants.IndexURI, // unused here but providing for completeness + }, + } + if os.Getenv(constants.EnableMultiIndexSwitch) != "" { + out, err := indexoperations.ListIndexes(paths) + if err != nil { + return errors.Wrapf(err, "failed to list plugin indexes available") + } + indexes = out } - names := make([]string, len(plugins)) - pluginMap := make(map[string]index.Plugin, len(plugins)) + + klog.V(3).Infof("found %d indexes", len(indexes)) + + var plugins []pluginEntry + for _, idx := range indexes { + ps, err := indexscanner.LoadPluginListFromFS(paths.IndexPluginsPath(idx.Name)) + if err != nil { + return errors.Wrap(err, "failed to load the list of plugins from the index") + } + for _, p := range ps { + plugins = append(plugins, pluginEntry{p, idx.Name}) + } + } + + pluginCanonicalNames := make([]string, len(plugins)) + pluginCanonicalNameMap := make(map[string]pluginEntry, len(plugins)) for i, p := range plugins { - names[i] = p.Name - pluginMap[p.Name] = p + cn := canonicalName(p.p, p.indexName) + pluginCanonicalNames[i] = cn + pluginCanonicalNameMap[cn] = p } + installed := make(map[string]bool) receipts, err := installation.GetInstalledPluginReceipts(paths.InstallReceiptsPath()) if err != nil { return errors.Wrap(err, "failed to load installed plugins") } - - // TODO(chriskim06) include index name when refactoring for custom indexes - installed := make(map[string]string) for _, receipt := range receipts { - installed[receipt.Name] = receipt.Spec.Version + cn := canonicalName(receipt.Plugin, indexOf(receipt)) + installed[cn] = true } - var matchNames []string + var searchResults []string if len(args) > 0 { - matches := fuzzy.Find(strings.Join(args, ""), names) + matches := fuzzy.Find(strings.Join(args, ""), pluginCanonicalNames) for _, m := range matches { - matchNames = append(matchNames, m.Str) + searchResults = append(searchResults, m.Str) } } else { - matchNames = names + searchResults = pluginCanonicalNames } // No plugins found - if len(matchNames) == 0 { + if len(searchResults) == 0 { return nil } var rows [][]string cols := []string{"NAME", "DESCRIPTION", "INSTALLED"} - for _, name := range matchNames { - plugin := pluginMap[name] + for _, canonicalName := range searchResults { + v := pluginCanonicalNameMap[canonicalName] var status string - if _, ok := installed[name]; ok { + if installed[canonicalName] { status = "yes" - } else if _, ok, err := installation.GetMatchingPlatform(plugin.Spec.Platforms); err != nil { - return errors.Wrapf(err, "failed to get the matching platform for plugin %s", name) + } else if _, ok, err := installation.GetMatchingPlatform(v.p.Spec.Platforms); err != nil { + return errors.Wrapf(err, "failed to get the matching platform for plugin %s", canonicalName) } else if ok { status = "no" } else { status = "unavailable on " + runtime.GOOS } - rows = append(rows, []string{name, limitString(plugin.Spec.ShortDescription, 50), status}) + + rows = append(rows, []string{displayName(v.p, v.indexName), limitString(v.p.Spec.ShortDescription, 50), status}) } rows = sortByFirstColumn(rows) return printTable(os.Stdout, cols, rows) diff --git a/integration_test/search_test.go b/integration_test/search_test.go index 44489bec..a44de314 100644 --- a/integration_test/search_test.go +++ b/integration_test/search_test.go @@ -15,8 +15,12 @@ package integrationtest import ( + "regexp" + "sort" "strings" "testing" + + "sigs.k8s.io/krew/pkg/constants" ) func TestKrewSearchAll(t *testing.T) { @@ -46,3 +50,52 @@ func TestKrewSearchOne(t *testing.T) { t.Errorf("The first match should be krew") } } + +func TestKrewSearchMultiIndex(t *testing.T) { + skipShort(t) + test, cleanup := NewTest(t) + test = test.WithEnv(constants.EnableMultiIndexSwitch, 1).WithIndex() + defer cleanup() + + // alias default plugin index to another + localIndex := test.TempDir().Path("index/" + constants.DefaultIndexName) + test.Krew("index", "add", "foo", localIndex).RunOrFailOutput() + + test.Krew("install", validPlugin).RunOrFail() + test.Krew("install", "foo/"+validPlugin2).RunOrFail() + + output := string(test.Krew("search").RunOrFailOutput()) + wantPatterns := []*regexp.Regexp{ + regexp.MustCompile(`(?m)^` + validPlugin + `\b.*\byes`), + regexp.MustCompile(`(?m)^` + validPlugin2 + `\b.*\bno`), + regexp.MustCompile(`(?m)^foo/` + validPlugin + `\b.*\bno$`), + regexp.MustCompile(`(?m)^foo/` + validPlugin2 + `\b.*\byes$`), + } + for _, p := range wantPatterns { + if !p.MatchString(output) { + t.Fatalf("pattern %s not found in search output=%s", p, output) + } + } +} + +func TestKrewSearchMultiIndexSortedByDisplayName(t *testing.T) { + skipShort(t) + test, cleanup := NewTest(t) + test = test.WithEnv(constants.EnableMultiIndexSwitch, 1).WithIndex() + defer cleanup() + + // alias default plugin index to another + localIndex := test.TempDir().Path("index/" + constants.DefaultIndexName) + test.Krew("index", "add", "foo", localIndex).RunOrFailOutput() + + output := string(test.Krew("search").RunOrFailOutput()) + + // match first column that is not NAME by matching everything up until a space + names := regexp.MustCompile(`(?m)^[^\s|NAME]+\b`).FindAllString(output, -1) + if len(names) < 10 { + t.Fatalf("could not capture names") + } + if !sort.StringsAreSorted(names) { + t.Fatalf("names are not sorted: [%s]", strings.Join(names, ", ")) + } +}