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 c85de71
Show file tree
Hide file tree
Showing 4 changed files with 274 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
50 changes: 50 additions & 0 deletions cmd/krew/cmd/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Copyright 2019 The Kubernetes Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cmd

import (
"sigs.k8s.io/krew/pkg/constants"
"sigs.k8s.io/krew/pkg/index"
)

// indexOf returns the index name of a receipt.
func indexOf(r index.Receipt) string {
if r.Status.Source.Name == "" {
return constants.DefaultIndexName
}
return r.Status.Source.Name
}

// displayName returns the display name of a Plugin.
// The index name is omitted if it is the default index.
func displayName(p index.Plugin, indexName string) string {
if isDefaultIndex(indexName) {
return p.Name
}
return indexName + "/" + p.Name
}

func isDefaultIndex(name string) bool {
return name == "" || name == constants.DefaultIndexName
}

// canonicalName returns INDEX/NAME value for a plugin, even if
// it is in the default index.
func canonicalName(p index.Plugin, indexName string) string {
if isDefaultIndex(indexName) {
indexName = constants.DefaultIndexName
}
return indexName + "/" + p.Name
}
114 changes: 114 additions & 0 deletions cmd/krew/cmd/util_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright 2019 The Kubernetes Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cmd

import (
"testing"

"github.com/google/go-cmp/cmp"

"sigs.k8s.io/krew/internal/testutil"
"sigs.k8s.io/krew/pkg/constants"
"sigs.k8s.io/krew/pkg/index"
)

func Test_isDefaultIndex(t *testing.T) {
if !isDefaultIndex("") {
t.Error("empty string must indicate default index")
}
if !isDefaultIndex("default") { // nb: intentionally not using the const to ensure compatibility
t.Error("default index must indicate default index")
}
if isDefaultIndex("foo") { // nb: intentionally not using the const to ensure compatibility
t.Error("name=foo must not indicate default index")
}
}

func TestIndexOf(t *testing.T) {
noIndex := testutil.NewReceipt().WithPlugin(testutil.NewPlugin().V()).WithStatus(index.ReceiptStatus{}).V()
if got := indexOf(noIndex); got != constants.DefaultIndexName {
t.Errorf("expected default index for no index in status; got=%q", got)
}
defaultIndex := testutil.NewReceipt().WithPlugin(testutil.NewPlugin().V()).V()
if got := indexOf(defaultIndex); got != constants.DefaultIndexName {
t.Errorf("expected 'default' for default index; got=%q", got)
}
customIndex := testutil.NewReceipt().WithPlugin(testutil.NewPlugin().V()).WithStatus(
index.ReceiptStatus{Source: index.SourceIndex{Name: "foo"}}).V()
if got := indexOf(customIndex); got != "foo" {
t.Errorf("expected custom index name; got=%q", got)
}
}

func Test_displayName(t *testing.T) {
type args struct {
p index.Plugin
index string
}
tests := []struct {
name string
in args
expected string
}{
{
name: "explicit default index",
in: args{
p: testutil.NewPlugin().WithName("foo").V(),
index: constants.DefaultIndexName,
},
expected: "foo",
},
{
name: "no index",
in: args{
p: testutil.NewPlugin().WithName("foo").V(),
index: "",
},
expected: "foo",
},
{
name: "custom index",
in: args{
p: testutil.NewPlugin().WithName("bar").V(),
index: "foo",
},
expected: "foo/bar",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
actual := displayName(tt.in.p, tt.in.index)
if diff := cmp.Diff(tt.expected, actual); diff != "" {
t.Fatalf("expected name to match: %s", diff)
}
})
}
}

func Test_canonicalName(t *testing.T) {
p1 := testutil.NewPlugin().WithName("foo").V()
if expected, got := "default/foo", canonicalName(p1, ""); got != expected {
t.Errorf("expected=%q; got=%q", expected, got)
}
p2 := testutil.NewPlugin().WithName("bar").V()
if expected, got := "default/bar", canonicalName(p2, "default"); got != expected {
t.Errorf("expected=%q; got=%q", expected, got)
}
p3 := testutil.NewPlugin().WithName("quux").V()
if expected, got := "custom/quux", canonicalName(p3, "custom"); got != expected {
t.Errorf("expected=%q; got=%q", expected, got)
}
}
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 c85de71

Please sign in to comment.