Skip to content

Commit

Permalink
cmd: make krew search work with multiple indexes
Browse files Browse the repository at this point in the history
TODO: the code right now has some inline methods to canonicalize Plugin/Receipt
names and create a display name for Plugin. Those should have a centralized
place soon.

Signed-off-by: Ahmet Alp Balkan <ahmetb@google.com>
  • Loading branch information
ahmetb committed Mar 29, 2020
1 parent 84d4410 commit 9d0b698
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 24 deletions.
94 changes: 70 additions & 24 deletions cmd/krew/cmd/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,20 @@
package cmd

import (
"fmt"
"os"
"runtime"
"strings"

"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,60 +45,104 @@ 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))
for i, p := range plugins {
names[i] = p.Name
pluginMap[p.Name] = p

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

// TODO(ahmetb): we could use this as a method on pluginEntry (nb: this is different than display name intentionally)
mkCanonicalName := func(v pluginEntry) string { return fmt.Sprintf("%s/%s", v.indexName, v.p.Name) }

// TODO(ahmetb): we could use this as a method on pluginEntry
displayName := func(v pluginEntry) string {
if v.indexName == constants.DefaultIndexName {
return v.p.Name
}
return mkCanonicalName(v)
}

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[mkCanonicalName(p)] = 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), limitString(v.p.Spec.ShortDescription, 50), status})
}
rows = sortByFirstColumn(rows)
//rows = sortByFirstColumn(rows)
return printTable(os.Stdout, cols, rows)
},
PreRunE: checkIndex,
Expand Down
54 changes: 54 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,53 @@ 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 9d0b698

Please sign in to comment.