mirror of https://github.com/helm/helm
Merge pull request #6211 from mattfarina/hub-search
Exposing Helm Hub search via the search commandpull/6219/head
commit
63b751ded7
@ -0,0 +1,102 @@
|
||||
/*
|
||||
Copyright The Helm 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 main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/gosuri/uitable"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"helm.sh/helm/internal/monocular"
|
||||
)
|
||||
|
||||
const searchHubDesc = `
|
||||
Search the Helm Hub or an instance of Monocular for Helm charts.
|
||||
|
||||
The Helm Hub provides a centralized search for publicly available distributed
|
||||
charts. It is maintained by the Helm project. It can be visited at
|
||||
https://hub.helm.sh
|
||||
|
||||
Monocular is a web-based application that enables the search and discovery of
|
||||
charts from multiple Helm Chart repositories. It is the codebase that powers the
|
||||
Helm Hub. You can find it at https://github.com/helm/monocular
|
||||
`
|
||||
|
||||
type searchHubOptions struct {
|
||||
searchEndpoint string
|
||||
maxColWidth uint
|
||||
}
|
||||
|
||||
func newSearchHubCmd(out io.Writer) *cobra.Command {
|
||||
o := &searchHubOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "hub [keyword]",
|
||||
Short: "search for charts in the Helm Hub or an instance of Monocular",
|
||||
Long: searchHubDesc,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.run(out, args)
|
||||
},
|
||||
}
|
||||
|
||||
f := cmd.Flags()
|
||||
f.StringVar(&o.searchEndpoint, "endpoint", "https://hub.helm.sh", "monocular instance to query for charts")
|
||||
f.UintVar(&o.maxColWidth, "max-col-width", 50, "maximum column width for output table")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *searchHubOptions) run(out io.Writer, args []string) error {
|
||||
|
||||
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 *searchHubOptions) formatSearchResults(endpoint string, res []monocular.SearchResult) string {
|
||||
if len(res) == 0 {
|
||||
return "No results found"
|
||||
}
|
||||
table := uitable.New()
|
||||
|
||||
// The max column width is configurable because a URL could be longer than the
|
||||
// max value and we want the user to have the ability to display the whole url
|
||||
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()
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
/*
|
||||
Copyright The Helm 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 main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSearchHubCmd(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 hub --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)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,163 @@
|
||||
/*
|
||||
Copyright The Helm 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 main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/semver"
|
||||
"github.com/gosuri/uitable"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"helm.sh/helm/cmd/helm/search"
|
||||
"helm.sh/helm/pkg/helmpath"
|
||||
"helm.sh/helm/pkg/repo"
|
||||
)
|
||||
|
||||
const searchRepoDesc = `
|
||||
Search reads through all of the repositories configured on the system, and
|
||||
looks for matches. Search of these repositories uses the metadata stored on
|
||||
the system.
|
||||
|
||||
Repositories are managed with 'helm repo' commands.
|
||||
`
|
||||
|
||||
// searchMaxScore suggests that any score higher than this is not considered a match.
|
||||
const searchMaxScore = 25
|
||||
|
||||
type searchRepoOptions struct {
|
||||
versions bool
|
||||
regexp bool
|
||||
version string
|
||||
maxColWidth uint
|
||||
}
|
||||
|
||||
func newSearchRepoCmd(out io.Writer) *cobra.Command {
|
||||
o := &searchRepoOptions{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "repo [keyword]",
|
||||
Short: "search repositories for a keyword in charts",
|
||||
Long: searchRepoDesc,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return o.run(out, args)
|
||||
},
|
||||
}
|
||||
|
||||
f := cmd.Flags()
|
||||
f.BoolVarP(&o.regexp, "regexp", "r", 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, "max-col-width", 50, "maximum column width for output table")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (o *searchRepoOptions) run(out io.Writer, args []string) error {
|
||||
index, err := o.buildIndex(out)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var res []*search.Result
|
||||
if len(args) == 0 {
|
||||
res = index.All()
|
||||
} else {
|
||||
q := strings.Join(args, " ")
|
||||
res, err = index.Search(q, searchMaxScore, o.regexp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
search.SortScore(res)
|
||||
data, err := o.applyConstraint(res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(out, o.formatSearchResults(data))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *searchRepoOptions) applyConstraint(res []*search.Result) ([]*search.Result, error) {
|
||||
if len(o.version) == 0 {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
constraint, err := semver.NewConstraint(o.version)
|
||||
if err != nil {
|
||||
return res, errors.Wrap(err, "an invalid version/constraint format")
|
||||
}
|
||||
|
||||
data := res[:0]
|
||||
foundNames := map[string]bool{}
|
||||
for _, r := range res {
|
||||
if _, found := foundNames[r.Name]; found {
|
||||
continue
|
||||
}
|
||||
v, err := semver.NewVersion(r.Chart.Version)
|
||||
if err != nil || constraint.Check(v) {
|
||||
data = append(data, r)
|
||||
if !o.versions {
|
||||
foundNames[r.Name] = true // If user hasn't requested all versions, only show the latest that matches
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (o *searchRepoOptions) formatSearchResults(res []*search.Result) string {
|
||||
if len(res) == 0 {
|
||||
return "No results found"
|
||||
}
|
||||
table := uitable.New()
|
||||
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)
|
||||
}
|
||||
return table.String()
|
||||
}
|
||||
|
||||
func (o *searchRepoOptions) buildIndex(out io.Writer) (*search.Index, error) {
|
||||
// Load the repositories.yaml
|
||||
rf, err := repo.LoadFile(helmpath.RepositoryFile())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
i := search.NewIndex()
|
||||
for _, re := range rf.Repositories {
|
||||
n := re.Name
|
||||
f := helmpath.CacheIndex(n)
|
||||
ind, err := repo.LoadIndexFile(f)
|
||||
if err != nil {
|
||||
// TODO should print to stderr
|
||||
fmt.Fprintf(out, "WARNING: Repo %q is corrupt or missing. Try 'helm repo update'.", n)
|
||||
continue
|
||||
}
|
||||
|
||||
i.AddRepo(n, ind, o.versions || len(o.version) > 0)
|
||||
}
|
||||
return i, nil
|
||||
}
|
Loading…
Reference in new issue