Exposing Helm Hub search via the search command

This retains the ability to search added repositories

Part of #6186

Signed-off-by: Matt Farina <matt@mattfarina.com>
pull/6211/head
Matt Farina 5 years ago
parent ac43d9faf2
commit d30d3f6218
No known key found for this signature in database
GPG Key ID: 9436E80BFBA46909

@ -27,6 +27,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/cmd/helm/search"
"helm.sh/helm/internal/monocular"
"helm.sh/helm/pkg/helmpath"
"helm.sh/helm/pkg/repo"
)
@ -42,9 +43,12 @@ Repositories are managed with 'helm repo' commands.
const searchMaxScore = 25
type searchOptions struct {
versions bool
regexp bool
version string
versions bool
regexp bool
version string
searchEndpoint string
repositories bool
maxColWidth uint
}
func newSearchCmd(out io.Writer) *cobra.Command {
@ -60,14 +64,62 @@ func newSearchCmd(out io.Writer) *cobra.Command {
}
f := cmd.Flags()
f.BoolVarP(&o.regexp, "regexp", "r", false, "use regular expressions for searching")
f.BoolVarP(&o.versions, "versions", "l", false, "show the long listing, with each version of each chart on its own line")
f.StringVar(&o.version, "version", "", "search using semantic versioning constraints")
f.StringVar(&o.searchEndpoint, "endpoint", "https://hub.helm.sh", "monocular instance to query for charts")
f.BoolVarP(&o.repositories, "repositories", "r", false, "search repositories you have added instead of monocular")
f.BoolVarP(&o.regexp, "regexp", "", false, "use regular expressions for searching repositories you have added")
f.BoolVarP(&o.versions, "versions", "l", false, "show the long listing, with each version of each chart on its own line, for repositories you have added")
f.StringVar(&o.version, "version", "", "search using semantic versioning constraints on repositories you have added")
f.UintVar(&o.maxColWidth, "maxColumnWidth", 50, "maximum column width for output table")
return cmd
}
func (o *searchOptions) run(out io.Writer, args []string) error {
// If searching the repositories added via helm repo add follow that path
if o.repositories {
debug("searching repositories")
// If an endpoint was passed in but searching repositories the user should
// know the option is being skipped
if o.searchEndpoint != "https://hub.helm.sh" {
fmt.Fprintln(out, "Notice: Setting the \"endpoint\" flag has no effect when searching repositories you have added")
}
return o.runRepositories(out, args)
}
// Search the Helm Hub or other monocular instance
debug("searching monocular")
// If an an option used against repository searches is used the user should
// know the option is being skipped
if o.regexp {
fmt.Fprintln(out, "Notice: Setting the \"regexp\" flag has no effect when searching monocular (e.g., Helm Hub)")
}
if o.versions {
fmt.Fprintln(out, "Notice: Setting the \"versions\" flag has no effect when searching monocular (e.g., Helm Hub)")
}
if o.version != "" {
fmt.Fprintln(out, "Notice: Setting the \"version\" flag has no effect when searching monocular (e.g., Helm Hub)")
}
c, err := monocular.New(o.searchEndpoint)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("unable to create connection to %q", o.searchEndpoint))
}
q := strings.Join(args, " ")
results, err := c.Search(q)
if err != nil {
debug("%s", err)
return fmt.Errorf("unable to perform search against %q", o.searchEndpoint)
}
fmt.Fprintln(out, o.formatSearchResults(o.searchEndpoint, results))
return nil
}
func (o *searchOptions) runRepositories(out io.Writer, args []string) error {
index, err := o.buildIndex(out)
if err != nil {
return err
@ -90,7 +142,7 @@ func (o *searchOptions) run(out io.Writer, args []string) error {
return err
}
fmt.Fprintln(out, o.formatSearchResults(data))
fmt.Fprintln(out, o.formatRepoSearchResults(data))
return nil
}
@ -123,12 +175,27 @@ func (o *searchOptions) applyConstraint(res []*search.Result) ([]*search.Result,
return data, nil
}
func (o *searchOptions) formatSearchResults(res []*search.Result) string {
func (o *searchOptions) formatSearchResults(endpoint string, res []monocular.SearchResult) string {
if len(res) == 0 {
return "No results found"
}
table := uitable.New()
table.MaxColWidth = o.maxColWidth
table.AddRow("URL", "CHART VERSION", "APP VERSION", "DESCRIPTION")
var url string
for _, r := range res {
url = endpoint + "/charts/" + r.ID
table.AddRow(url, r.Relationships.LatestChartVersion.Data.Version, r.Relationships.LatestChartVersion.Data.AppVersion, r.Attributes.Description)
}
return table.String()
}
func (o *searchOptions) formatRepoSearchResults(res []*search.Result) string {
if len(res) == 0 {
return "No results found"
}
table := uitable.New()
table.MaxColWidth = 50
table.MaxColWidth = o.maxColWidth
table.AddRow("NAME", "CHART VERSION", "APP VERSION", "DESCRIPTION")
for _, r := range res {
table.AddRow(r.Name, r.Chart.Version, r.Chart.AppVersion, r.Chart.Description)

@ -17,13 +17,45 @@ limitations under the License.
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"testing"
"helm.sh/helm/pkg/helmpath/xdg"
)
func TestSearchCmd(t *testing.T) {
func TestSearchMonocularCmd(t *testing.T) {
// Setup a mock search service
var searchResult = `{"data":[{"id":"stable/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"stable","url":"https://kubernetes-charts.storage.googleapis.com"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/stable/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T17:57:31.38Z","digest":"119c499251bffd4b06ff0cd5ac98c2ce32231f84899fb4825be6c2d90971c742","urls":["https://kubernetes-charts.storage.googleapis.com/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/stable/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/stable/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/stable/phpmyadmin/versions/3.0.0"}}}},{"id":"bitnami/phpmyadmin","type":"chart","attributes":{"name":"phpmyadmin","repo":{"name":"bitnami","url":"https://charts.bitnami.com"},"description":"phpMyAdmin is an mysql administration frontend","home":"https://www.phpmyadmin.net/","keywords":["mariadb","mysql","phpmyadmin"],"maintainers":[{"name":"Bitnami","email":"containers@bitnami.com"}],"sources":["https://github.com/bitnami/bitnami-docker-phpmyadmin"],"icon":""},"links":{"self":"/v1/charts/bitnami/phpmyadmin"},"relationships":{"latestChartVersion":{"data":{"version":"3.0.0","app_version":"4.9.0-1","created":"2019-08-08T18:34:13.341Z","digest":"66d77cf6d8c2b52c488d0a294cd4996bd5bad8dc41d3829c394498fb401c008a","urls":["https://charts.bitnami.com/bitnami/phpmyadmin-3.0.0.tgz"],"readme":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/README.md","values":"/v1/assets/bitnami/phpmyadmin/versions/3.0.0/values.yaml"},"links":{"self":"/v1/charts/bitnami/phpmyadmin/versions/3.0.0"}}}}]}`
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, searchResult)
}))
defer ts.Close()
// The expected output has the URL to the mocked search service in it
var expected = fmt.Sprintf(`URL CHART VERSION APP VERSION DESCRIPTION
%s/charts/stable/phpmyadmin 3.0.0 4.9.0-1 phpMyAdmin is an mysql administration frontend
%s/charts/bitnami/phpmyadmin 3.0.0 4.9.0-1 phpMyAdmin is an mysql administration frontend
`, ts.URL, ts.URL)
testcmd := "search --endpoint " + ts.URL + " maria"
storage := storageFixture()
_, out, err := executeActionCommandC(storage, testcmd)
if err != nil {
t.Errorf("unexpected error, %s", err)
}
if out != expected {
t.Error("expected and actual output did not match")
t.Log(out)
t.Log(expected)
}
}
func TestSearchRepositoriesCmd(t *testing.T) {
defer resetEnv()()
os.Setenv(xdg.CacheHomeEnvVar, "testdata/helmhome")
@ -32,43 +64,43 @@ func TestSearchCmd(t *testing.T) {
tests := []cmdTestCase{{
name: "search for 'maria', expect one match",
cmd: "search maria",
cmd: "search -r maria",
golden: "output/search-single.txt",
}, {
name: "search for 'alpine', expect two matches",
cmd: "search alpine",
cmd: "search -r alpine",
golden: "output/search-multiple.txt",
}, {
name: "search for 'alpine' with versions, expect three matches",
cmd: "search alpine --versions",
cmd: "search -r alpine --versions",
golden: "output/search-multiple-versions.txt",
}, {
name: "search for 'alpine' with version constraint, expect one match with version 0.1.0",
cmd: "search alpine --version '>= 0.1, < 0.2'",
cmd: "search -r alpine --version '>= 0.1, < 0.2'",
golden: "output/search-constraint.txt",
}, {
name: "search for 'alpine' with version constraint, expect one match with version 0.1.0",
cmd: "search alpine --versions --version '>= 0.1, < 0.2'",
cmd: "search -r alpine --versions --version '>= 0.1, < 0.2'",
golden: "output/search-versions-constraint.txt",
}, {
name: "search for 'alpine' with version constraint, expect one match with version 0.2.0",
cmd: "search alpine --version '>= 0.1'",
cmd: "search -r alpine --version '>= 0.1'",
golden: "output/search-constraint-single.txt",
}, {
name: "search for 'alpine' with version constraint and --versions, expect two matches",
cmd: "search alpine --versions --version '>= 0.1'",
cmd: "search -r alpine --versions --version '>= 0.1'",
golden: "output/search-multiple-versions-constraints.txt",
}, {
name: "search for 'syzygy', expect no matches",
cmd: "search syzygy",
cmd: "search -r syzygy",
golden: "output/search-not-found.txt",
}, {
name: "search for 'alp[a-z]+', expect two matches",
cmd: "search alp[a-z]+ --regexp",
cmd: "search -r alp[a-z]+ --regexp",
golden: "output/search-regex.txt",
}, {
name: "search for 'alp[', expect failure to compile regexp",
cmd: "search alp[ --regexp",
cmd: "search -r alp[ --regexp",
wantError: true,
}}
runTestCmd(t, tests)

Loading…
Cancel
Save