Skip to content

Commit

Permalink
cmd: make krew search work with multiple indexes
Browse files Browse the repository at this point in the history
Mostly just refactoring.
Added test to make sure search output is alphabetically sorted (even in case
of multiple indexes).

Signed-off-by: Ahmet Alp Balkan <[email protected]>
  • Loading branch information
ahmetb committed Mar 30, 2020
1 parent 532d7fa commit 42acce5
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 23 deletions.
80 changes: 57 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,91 @@ 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
},
}
names := make([]string, len(plugins))
pluginMap := make(map[string]index.Plugin, len(plugins))
for i, p := range plugins {
names[i] = p.Name
pluginMap[p.Name] = p
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
}

klog.V(3).Infof("found %d indexes", len(indexes))

var plugins []pluginEntry
for _, idx := range indexes {
ps, err := indexscanner.LoadPluginListFromFS(paths.IndexPluginsPath(constants.DefaultIndexName))
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})
}
}

keys := func(v map[string]pluginEntry) []string {
out := make([]string, 0, len(v))
for k := range v {
out = append(out, k)
}
return out
}

pluginMap := make(map[string]pluginEntry, len(plugins))
for _, p := range plugins {
pluginMap[canonicalName(p.p, p.indexName)] = p
}

installed := make(map[string]string)
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
index := receipt.Status.Source.Name
if index == "" {
index = constants.DefaultIndexName
}
installed[receipt.Name] = index
}

var matchNames []string
corpus := keys(pluginMap)
var searchResults []string
if len(args) > 0 {
matches := fuzzy.Find(strings.Join(args, ""), names)
matches := fuzzy.Find(strings.Join(args, ""), corpus)
for _, m := range matches {
matchNames = append(matchNames, m.Str)
searchResults = append(searchResults, m.Str)
}
} else {
matchNames = names
searchResults = corpus
}

// 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 _, name := range searchResults {
v := pluginMap[name]
var status string
if _, ok := installed[name]; ok {
if index := installed[v.p.Name]; index == v.indexName {
status = "yes"
} else if _, ok, err := installation.GetMatchingPlatform(plugin.Spec.Platforms); err != nil {
} 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", name)
} 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) {
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, ", "))
}
}

0 comments on commit 42acce5

Please sign in to comment.