feat(helm): add --versions flag on search

This causes search to index by name/version instead of just name, which
means you can get a list of versions of a chart. The '--versions' flag
enables this behavior.

Partially fixes #1199
pull/1284/head
Matt Butcher 8 years ago
parent ea66d66d2d
commit d0cefeaf82

@ -43,7 +43,8 @@ type searchCmd struct {
out io.Writer out io.Writer
helmhome helmpath.Home helmhome helmpath.Home
regexp bool versions bool
regexp bool
} }
func newSearchCmd(out io.Writer) *cobra.Command { func newSearchCmd(out io.Writer) *cobra.Command {
@ -59,7 +60,9 @@ func newSearchCmd(out io.Writer) *cobra.Command {
PreRunE: requireInit, PreRunE: requireInit,
} }
cmd.Flags().BoolVarP(&sc.regexp, "regexp", "r", false, "use regular expressions for searching") f := cmd.Flags()
f.BoolVarP(&sc.regexp, "regexp", "r", false, "use regular expressions for searching")
f.BoolVarP(&sc.versions, "versions", "l", false, "show the long listing, with each version of each chart on its own line.")
return cmd return cmd
} }
@ -88,16 +91,7 @@ func (s *searchCmd) run(args []string) error {
} }
func (s *searchCmd) showAllCharts(i *search.Index) { func (s *searchCmd) showAllCharts(i *search.Index) {
e := i.Entries() res := i.All()
res := make([]*search.Result, len(e))
j := 0
for name, ch := range e {
res[j] = &search.Result{
Name: name,
Chart: ch,
}
j++
}
search.SortScore(res) search.SortScore(res)
fmt.Fprintln(s.out, s.formatSearchResults(res)) fmt.Fprintln(s.out, s.formatSearchResults(res))
} }
@ -129,7 +123,7 @@ func (s *searchCmd) buildIndex() (*search.Index, error) {
continue continue
} }
i.AddRepo(n, ind) i.AddRepo(n, ind, s.versions)
} }
return i, nil return i, nil
} }

@ -29,6 +29,8 @@ import (
"sort" "sort"
"strings" "strings"
"github.com/Masterminds/semver"
"k8s.io/helm/pkg/repo" "k8s.io/helm/pkg/repo"
) )
@ -55,25 +57,51 @@ func NewIndex() *Index {
return &Index{lines: map[string]string{}, charts: map[string]*repo.ChartVersion{}} return &Index{lines: map[string]string{}, charts: map[string]*repo.ChartVersion{}}
} }
// verSep is a separator for version fields in map keys.
const verSep = "$$"
// AddRepo adds a repository index to the search index. // AddRepo adds a repository index to the search index.
func (i *Index) AddRepo(rname string, ind *repo.IndexFile) { func (i *Index) AddRepo(rname string, ind *repo.IndexFile, all bool) {
for name, ref := range ind.Entries { for name, ref := range ind.Entries {
if len(ref) == 0 { if len(ref) == 0 {
// Skip chart names that havae zero releases. // Skip chart names that have zero releases.
continue continue
} }
// By convention, an index file is supposed to have the newest at the // By convention, an index file is supposed to have the newest at the
// 0 slot, so our best bet is to grab the 0 entry and build the index // 0 slot, so our best bet is to grab the 0 entry and build the index
// entry off of that. // entry off of that.
fname := filepath.Join(rname, name) fname := filepath.Join(rname, name)
i.lines[fname] = indstr(rname, ref[0]) if !all {
i.charts[fname] = ref[0] i.lines[fname] = indstr(rname, ref[0])
i.charts[fname] = ref[0]
continue
}
// If 'all' is set, then we go through all of the refs, and add them all
// to the index. This will generate a lot of near-duplicate entries.
for _, rr := range ref {
versionedName := fname + verSep + rr.Version
i.lines[versionedName] = indstr(rname, rr)
i.charts[versionedName] = rr
}
} }
} }
// Entries returns the entries in an index. // All returns all charts in the index as if they were search results.
func (i *Index) Entries() map[string]*repo.ChartVersion { //
return i.charts // Each will be given a score of 0.
func (i *Index) All() []*Result {
res := make([]*Result, len(i.charts))
j := 0
for name, ch := range i.charts {
parts := strings.Split(name, verSep)
res[j] = &Result{
Name: parts[0],
Chart: ch,
}
j++
}
return res
} }
// Search searches an index for the given term. // Search searches an index for the given term.
@ -118,7 +146,8 @@ func (i *Index) SearchLiteral(term string, threshold int) []*Result {
for k, v := range i.lines { for k, v := range i.lines {
res := strings.Index(v, term) res := strings.Index(v, term)
if score := i.calcScore(res, v); res != -1 && score < threshold { if score := i.calcScore(res, v); res != -1 && score < threshold {
buf = append(buf, &Result{Name: k, Score: score, Chart: i.charts[k]}) parts := strings.Split(k, verSep) // Remove version, if it is there.
buf = append(buf, &Result{Name: parts[0], Score: score, Chart: i.charts[k]})
} }
} }
return buf return buf
@ -137,7 +166,8 @@ func (i *Index) SearchRegexp(re string, threshold int) ([]*Result, error) {
continue continue
} }
if score := i.calcScore(ind[0], v); ind[0] >= 0 && score < threshold { if score := i.calcScore(ind[0], v); ind[0] >= 0 && score < threshold {
buf = append(buf, &Result{Name: k, Score: score, Chart: i.charts[k]}) parts := strings.Split(k, verSep) // Remove version, if it is there.
buf = append(buf, &Result{Name: parts[0], Score: score, Chart: i.charts[k]})
} }
} }
return buf, nil return buf, nil
@ -179,6 +209,17 @@ func (s scoreSorter) Less(a, b int) bool {
if first.Score < second.Score { if first.Score < second.Score {
return true return true
} }
if first.Name == second.Name {
v1, err := semver.NewVersion(first.Chart.Version)
if err != nil {
return true
}
v2, err := semver.NewVersion(second.Chart.Version)
if err != nil {
return true
}
return v1.GreaterThan(v2)
}
return first.Name < second.Name return first.Name < second.Name
} }

@ -93,9 +93,9 @@ var indexfileEntries = map[string]repo.ChartVersions{
}, },
} }
func loadTestIndex(t *testing.T) *Index { func loadTestIndex(t *testing.T, all bool) *Index {
i := NewIndex() i := NewIndex()
i.AddRepo("testing", &repo.IndexFile{Entries: indexfileEntries}) i.AddRepo("testing", &repo.IndexFile{Entries: indexfileEntries}, all)
i.AddRepo("ztesting", &repo.IndexFile{Entries: map[string]repo.ChartVersions{ i.AddRepo("ztesting", &repo.IndexFile{Entries: map[string]repo.ChartVersions{
"pinta": { "pinta": {
{ {
@ -107,10 +107,24 @@ func loadTestIndex(t *testing.T) *Index {
}, },
}, },
}, },
}}) }}, all)
return i return i
} }
func TestAll(t *testing.T) {
i := loadTestIndex(t, false)
all := i.All()
if len(all) != 4 {
t.Errorf("Expected 4 entries, got %d", len(all))
}
i = loadTestIndex(t, true)
all = i.All()
if len(all) != 5 {
t.Errorf("Expected 5 entries, got %d", len(all))
}
}
func TestSearchByName(t *testing.T) { func TestSearchByName(t *testing.T) {
tests := []struct { tests := []struct {
@ -188,7 +202,7 @@ func TestSearchByName(t *testing.T) {
}, },
} }
i := loadTestIndex(t) i := loadTestIndex(t, false)
for _, tt := range tests { for _, tt := range tests {
@ -224,6 +238,18 @@ func TestSearchByName(t *testing.T) {
} }
} }
func TestSearchByNameAll(t *testing.T) {
// Test with the All bit turned on.
i := loadTestIndex(t, true)
cs, err := i.Search("santa-maria", 100, false)
if err != nil {
t.Fatal(err)
}
if len(cs) != 2 {
t.Errorf("expected 2 charts, got %d", len(cs))
}
}
func TestCalcScore(t *testing.T) { func TestCalcScore(t *testing.T) {
i := NewIndex() i := NewIndex()

@ -41,6 +41,12 @@ func TestSearchCmd(t *testing.T) {
args: []string{"alpine"}, args: []string{"alpine"},
expect: "NAME \tVERSION\tDESCRIPTION \ntesting/alpine\t0.1.0 \tDeploy a basic Alpine Linux pod", expect: "NAME \tVERSION\tDESCRIPTION \ntesting/alpine\t0.1.0 \tDeploy a basic Alpine Linux pod",
}, },
{
name: "search for 'alpine' with versions, expect three matches",
args: []string{"alpine"},
flags: []string{"--versions"},
expect: "NAME \tVERSION\tDESCRIPTION \ntesting/alpine\t0.2.0 \tDeploy a basic Alpine Linux pod\ntesting/alpine\t0.1.0 \tDeploy a basic Alpine Linux pod",
},
{ {
name: "search for 'syzygy', expect no matches", name: "search for 'syzygy', expect no matches",
args: []string{"syzygy"}, args: []string{"syzygy"},

Loading…
Cancel
Save