github.com/stefanmcshane/helm@v0.0.0-20221213002717-88a4a2c6e77d/cmd/helm/search_hub.go (about)

     1  /*
     2  Copyright The Helm Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package main
    18  
    19  import (
    20  	"fmt"
    21  	"io"
    22  	"strings"
    23  
    24  	"github.com/gosuri/uitable"
    25  	"github.com/pkg/errors"
    26  	"github.com/spf13/cobra"
    27  
    28  	"github.com/stefanmcshane/helm/internal/monocular"
    29  	"github.com/stefanmcshane/helm/pkg/cli/output"
    30  )
    31  
    32  const searchHubDesc = `
    33  Search for Helm charts in the Artifact Hub or your own hub instance.
    34  
    35  Artifact Hub is a web-based application that enables finding, installing, and
    36  publishing packages and configurations for CNCF projects, including publicly
    37  available distributed charts Helm charts. It is a Cloud Native Computing
    38  Foundation sandbox project. You can browse the hub at https://artifacthub.io/
    39  
    40  The [KEYWORD] argument accepts either a keyword string, or quoted string of rich
    41  query options. For rich query options documentation, see
    42  https://artifacthub.github.io/hub/api/?urls.primaryName=Monocular%20compatible%20search%20API#/Monocular/get_api_chartsvc_v1_charts_search
    43  
    44  Previous versions of Helm used an instance of Monocular as the default
    45  'endpoint', so for backwards compatibility Artifact Hub is compatible with the
    46  Monocular search API. Similarly, when setting the 'endpoint' flag, the specified
    47  endpoint must also be implement a Monocular compatible search API endpoint.
    48  Note that when specifying a Monocular instance as the 'endpoint', rich queries
    49  are not supported. For API details, see https://github.com/helm/monocular
    50  `
    51  
    52  type searchHubOptions struct {
    53  	searchEndpoint string
    54  	maxColWidth    uint
    55  	outputFormat   output.Format
    56  	listRepoURL    bool
    57  }
    58  
    59  func newSearchHubCmd(out io.Writer) *cobra.Command {
    60  	o := &searchHubOptions{}
    61  
    62  	cmd := &cobra.Command{
    63  		Use:   "hub [KEYWORD]",
    64  		Short: "search for charts in the Artifact Hub or your own hub instance",
    65  		Long:  searchHubDesc,
    66  		RunE: func(cmd *cobra.Command, args []string) error {
    67  			return o.run(out, args)
    68  		},
    69  	}
    70  
    71  	f := cmd.Flags()
    72  	f.StringVar(&o.searchEndpoint, "endpoint", "https://hub.helm.sh", "Hub instance to query for charts")
    73  	f.UintVar(&o.maxColWidth, "max-col-width", 50, "maximum column width for output table")
    74  	f.BoolVar(&o.listRepoURL, "list-repo-url", false, "print charts repository URL")
    75  
    76  	bindOutputFlag(cmd, &o.outputFormat)
    77  
    78  	return cmd
    79  }
    80  
    81  func (o *searchHubOptions) run(out io.Writer, args []string) error {
    82  	c, err := monocular.New(o.searchEndpoint)
    83  	if err != nil {
    84  		return errors.Wrap(err, fmt.Sprintf("unable to create connection to %q", o.searchEndpoint))
    85  	}
    86  
    87  	q := strings.Join(args, " ")
    88  	results, err := c.Search(q)
    89  	if err != nil {
    90  		debug("%s", err)
    91  		return fmt.Errorf("unable to perform search against %q", o.searchEndpoint)
    92  	}
    93  
    94  	return o.outputFormat.Write(out, newHubSearchWriter(results, o.searchEndpoint, o.maxColWidth, o.listRepoURL))
    95  }
    96  
    97  type hubChartRepo struct {
    98  	URL  string `json:"url"`
    99  	Name string `json:"name"`
   100  }
   101  
   102  type hubChartElement struct {
   103  	URL         string       `json:"url"`
   104  	Version     string       `json:"version"`
   105  	AppVersion  string       `json:"app_version"`
   106  	Description string       `json:"description"`
   107  	Repository  hubChartRepo `json:"repository"`
   108  }
   109  
   110  type hubSearchWriter struct {
   111  	elements    []hubChartElement
   112  	columnWidth uint
   113  	listRepoURL bool
   114  }
   115  
   116  func newHubSearchWriter(results []monocular.SearchResult, endpoint string, columnWidth uint, listRepoURL bool) *hubSearchWriter {
   117  	var elements []hubChartElement
   118  	for _, r := range results {
   119  		// Backwards compatibility for Monocular
   120  		url := endpoint + "/charts/" + r.ID
   121  
   122  		// Check for artifactHub compatibility
   123  		if r.ArtifactHub.PackageURL != "" {
   124  			url = r.ArtifactHub.PackageURL
   125  		}
   126  
   127  		elements = append(elements, hubChartElement{url, r.Relationships.LatestChartVersion.Data.Version, r.Relationships.LatestChartVersion.Data.AppVersion, r.Attributes.Description, hubChartRepo{URL: r.Attributes.Repo.URL, Name: r.Attributes.Repo.Name}})
   128  	}
   129  	return &hubSearchWriter{elements, columnWidth, listRepoURL}
   130  }
   131  
   132  func (h *hubSearchWriter) WriteTable(out io.Writer) error {
   133  	if len(h.elements) == 0 {
   134  		_, err := out.Write([]byte("No results found\n"))
   135  		if err != nil {
   136  			return fmt.Errorf("unable to write results: %s", err)
   137  		}
   138  		return nil
   139  	}
   140  	table := uitable.New()
   141  	table.MaxColWidth = h.columnWidth
   142  
   143  	if h.listRepoURL {
   144  		table.AddRow("URL", "CHART VERSION", "APP VERSION", "DESCRIPTION", "REPO URL")
   145  	} else {
   146  		table.AddRow("URL", "CHART VERSION", "APP VERSION", "DESCRIPTION")
   147  	}
   148  
   149  	for _, r := range h.elements {
   150  		if h.listRepoURL {
   151  			table.AddRow(r.URL, r.Version, r.AppVersion, r.Description, r.Repository.URL)
   152  		} else {
   153  			table.AddRow(r.URL, r.Version, r.AppVersion, r.Description)
   154  		}
   155  	}
   156  	return output.EncodeTable(out, table)
   157  }
   158  
   159  func (h *hubSearchWriter) WriteJSON(out io.Writer) error {
   160  	return h.encodeByFormat(out, output.JSON)
   161  }
   162  
   163  func (h *hubSearchWriter) WriteYAML(out io.Writer) error {
   164  	return h.encodeByFormat(out, output.YAML)
   165  }
   166  
   167  func (h *hubSearchWriter) encodeByFormat(out io.Writer, format output.Format) error {
   168  	// Initialize the array so no results returns an empty array instead of null
   169  	chartList := make([]hubChartElement, 0, len(h.elements))
   170  
   171  	for _, r := range h.elements {
   172  		chartList = append(chartList, hubChartElement{r.URL, r.Version, r.AppVersion, r.Description, r.Repository})
   173  	}
   174  
   175  	switch format {
   176  	case output.JSON:
   177  		return output.EncodeJSON(out, chartList)
   178  	case output.YAML:
   179  		return output.EncodeYAML(out, chartList)
   180  	}
   181  
   182  	// Because this is a non-exported function and only called internally by
   183  	// WriteJSON and WriteYAML, we shouldn't get invalid types
   184  	return nil
   185  }