Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cmd: make krew search work with multiple indexes #574

Merged
merged 3 commits into from
Apr 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 48 additions & 23 deletions cmd/krew/cmd/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
53 changes: 53 additions & 0 deletions integration_test/search_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@
package integrationtest

import (
"regexp"
"sort"
"strings"
"testing"

"sigs.k8s.io/krew/pkg/constants"
)

func TestKrewSearchAll(t *testing.T) {
Expand Down Expand Up @@ -46,3 +50,52 @@ func TestKrewSearchOne(t *testing.T) {
t.Errorf("The first match should be krew")
}
}

func TestKrewSearchMultiIndex(t *testing.T) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice tests!

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, ", "))
}
}