github.com/Racer159/helm-experiment@v0.0.0-20230822001441-1eb31183f614/src/search_repo.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 cmd
    18  
    19  import (
    20  	"bufio"
    21  	"bytes"
    22  	"fmt"
    23  	"io"
    24  	"os"
    25  	"path/filepath"
    26  	"strings"
    27  
    28  	"github.com/Masterminds/semver/v3"
    29  	"github.com/gosuri/uitable"
    30  	"github.com/pkg/errors"
    31  	"github.com/spf13/cobra"
    32  
    33  	"helm.sh/helm/v3/cmd/helm/search"
    34  	"helm.sh/helm/v3/pkg/cli/output"
    35  	"helm.sh/helm/v3/pkg/helmpath"
    36  	"helm.sh/helm/v3/pkg/repo"
    37  )
    38  
    39  const searchRepoDesc = `
    40  Search reads through all of the repositories configured on the system, and
    41  looks for matches. Search of these repositories uses the metadata stored on
    42  the system.
    43  
    44  It will display the latest stable versions of the charts found. If you
    45  specify the --devel flag, the output will include pre-release versions.
    46  If you want to search using a version constraint, use --version.
    47  
    48  Examples:
    49  
    50      # Search for stable release versions matching the keyword "nginx"
    51      $ helm search repo nginx
    52  
    53      # Search for release versions matching the keyword "nginx", including pre-release versions
    54      $ helm search repo nginx --devel
    55  
    56      # Search for the latest stable release for nginx-ingress with a major version of 1
    57      $ helm search repo nginx-ingress --version ^1.0.0
    58  
    59  Repositories are managed with 'helm repo' commands.
    60  `
    61  
    62  // searchMaxScore suggests that any score higher than this is not considered a match.
    63  const searchMaxScore = 25
    64  
    65  type searchRepoOptions struct {
    66  	versions     bool
    67  	regexp       bool
    68  	devel        bool
    69  	version      string
    70  	maxColWidth  uint
    71  	repoFile     string
    72  	repoCacheDir string
    73  	outputFormat output.Format
    74  }
    75  
    76  func newSearchRepoCmd(out io.Writer) *cobra.Command {
    77  	o := &searchRepoOptions{}
    78  
    79  	cmd := &cobra.Command{
    80  		Use:   "repo [keyword]",
    81  		Short: "search repositories for a keyword in charts",
    82  		Long:  searchRepoDesc,
    83  		RunE: func(cmd *cobra.Command, args []string) error {
    84  			o.repoFile = settings.RepositoryConfig
    85  			o.repoCacheDir = settings.RepositoryCache
    86  			return o.run(out, args)
    87  		},
    88  	}
    89  
    90  	f := cmd.Flags()
    91  	f.BoolVarP(&o.regexp, "regexp", "r", false, "use regular expressions for searching repositories you have added")
    92  	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")
    93  	f.BoolVar(&o.devel, "devel", false, "use development versions (alpha, beta, and release candidate releases), too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored")
    94  	f.StringVar(&o.version, "version", "", "search using semantic versioning constraints on repositories you have added")
    95  	f.UintVar(&o.maxColWidth, "max-col-width", 50, "maximum column width for output table")
    96  	bindOutputFlag(cmd, &o.outputFormat)
    97  
    98  	return cmd
    99  }
   100  
   101  func (o *searchRepoOptions) run(out io.Writer, args []string) error {
   102  	o.setupSearchedVersion()
   103  
   104  	index, err := o.buildIndex()
   105  	if err != nil {
   106  		return err
   107  	}
   108  
   109  	var res []*search.Result
   110  	if len(args) == 0 {
   111  		res = index.All()
   112  	} else {
   113  		q := strings.Join(args, " ")
   114  		res, err = index.Search(q, searchMaxScore, o.regexp)
   115  		if err != nil {
   116  			return err
   117  		}
   118  	}
   119  
   120  	search.SortScore(res)
   121  	data, err := o.applyConstraint(res)
   122  	if err != nil {
   123  		return err
   124  	}
   125  
   126  	return o.outputFormat.Write(out, &repoSearchWriter{data, o.maxColWidth})
   127  }
   128  
   129  func (o *searchRepoOptions) setupSearchedVersion() {
   130  	debug("Original chart version: %q", o.version)
   131  
   132  	if o.version != "" {
   133  		return
   134  	}
   135  
   136  	if o.devel { // search for releases and prereleases (alpha, beta, and release candidate releases).
   137  		debug("setting version to >0.0.0-0")
   138  		o.version = ">0.0.0-0"
   139  	} else { // search only for stable releases, prerelease versions will be skip
   140  		debug("setting version to >0.0.0")
   141  		o.version = ">0.0.0"
   142  	}
   143  }
   144  
   145  func (o *searchRepoOptions) applyConstraint(res []*search.Result) ([]*search.Result, error) {
   146  	if o.version == "" {
   147  		return res, nil
   148  	}
   149  
   150  	constraint, err := semver.NewConstraint(o.version)
   151  	if err != nil {
   152  		return res, errors.Wrap(err, "an invalid version/constraint format")
   153  	}
   154  
   155  	data := res[:0]
   156  	foundNames := map[string]bool{}
   157  	for _, r := range res {
   158  		// if not returning all versions and already have found a result,
   159  		// you're done!
   160  		if !o.versions && foundNames[r.Name] {
   161  			continue
   162  		}
   163  		v, err := semver.NewVersion(r.Chart.Version)
   164  		if err != nil {
   165  			continue
   166  		}
   167  		if constraint.Check(v) {
   168  			data = append(data, r)
   169  			foundNames[r.Name] = true
   170  		}
   171  	}
   172  
   173  	return data, nil
   174  }
   175  
   176  func (o *searchRepoOptions) buildIndex() (*search.Index, error) {
   177  	// Load the repositories.yaml
   178  	rf, err := repo.LoadFile(o.repoFile)
   179  	if isNotExist(err) || len(rf.Repositories) == 0 {
   180  		return nil, errors.New("no repositories configured")
   181  	}
   182  
   183  	i := search.NewIndex()
   184  	for _, re := range rf.Repositories {
   185  		n := re.Name
   186  		f := filepath.Join(o.repoCacheDir, helmpath.CacheIndexFile(n))
   187  		ind, err := repo.LoadIndexFile(f)
   188  		if err != nil {
   189  			warning("Repo %q is corrupt or missing. Try 'helm repo update'.", n)
   190  			warning("%s", err)
   191  			continue
   192  		}
   193  
   194  		i.AddRepo(n, ind, o.versions || len(o.version) > 0)
   195  	}
   196  	return i, nil
   197  }
   198  
   199  type repoChartElement struct {
   200  	Name        string `json:"name"`
   201  	Version     string `json:"version"`
   202  	AppVersion  string `json:"app_version"`
   203  	Description string `json:"description"`
   204  }
   205  
   206  type repoSearchWriter struct {
   207  	results     []*search.Result
   208  	columnWidth uint
   209  }
   210  
   211  func (r *repoSearchWriter) WriteTable(out io.Writer) error {
   212  	if len(r.results) == 0 {
   213  		_, err := out.Write([]byte("No results found\n"))
   214  		if err != nil {
   215  			return fmt.Errorf("unable to write results: %s", err)
   216  		}
   217  		return nil
   218  	}
   219  	table := uitable.New()
   220  	table.MaxColWidth = r.columnWidth
   221  	table.AddRow("NAME", "CHART VERSION", "APP VERSION", "DESCRIPTION")
   222  	for _, r := range r.results {
   223  		table.AddRow(r.Name, r.Chart.Version, r.Chart.AppVersion, r.Chart.Description)
   224  	}
   225  	return output.EncodeTable(out, table)
   226  }
   227  
   228  func (r *repoSearchWriter) WriteJSON(out io.Writer) error {
   229  	return r.encodeByFormat(out, output.JSON)
   230  }
   231  
   232  func (r *repoSearchWriter) WriteYAML(out io.Writer) error {
   233  	return r.encodeByFormat(out, output.YAML)
   234  }
   235  
   236  func (r *repoSearchWriter) encodeByFormat(out io.Writer, format output.Format) error {
   237  	// Initialize the array so no results returns an empty array instead of null
   238  	chartList := make([]repoChartElement, 0, len(r.results))
   239  
   240  	for _, r := range r.results {
   241  		chartList = append(chartList, repoChartElement{r.Name, r.Chart.Version, r.Chart.AppVersion, r.Chart.Description})
   242  	}
   243  
   244  	switch format {
   245  	case output.JSON:
   246  		return output.EncodeJSON(out, chartList)
   247  	case output.YAML:
   248  		return output.EncodeYAML(out, chartList)
   249  	}
   250  
   251  	// Because this is a non-exported function and only called internally by
   252  	// WriteJSON and WriteYAML, we shouldn't get invalid types
   253  	return nil
   254  }
   255  
   256  // Provides the list of charts that are part of the specified repo, and that starts with 'prefix'.
   257  func compListChartsOfRepo(repoName string, prefix string) []string {
   258  	var charts []string
   259  
   260  	path := filepath.Join(settings.RepositoryCache, helmpath.CacheChartsFile(repoName))
   261  	content, err := os.ReadFile(path)
   262  	if err == nil {
   263  		scanner := bufio.NewScanner(bytes.NewReader(content))
   264  		for scanner.Scan() {
   265  			fullName := fmt.Sprintf("%s/%s", repoName, scanner.Text())
   266  			if strings.HasPrefix(fullName, prefix) {
   267  				charts = append(charts, fullName)
   268  			}
   269  		}
   270  		return charts
   271  	}
   272  
   273  	if isNotExist(err) {
   274  		// If there is no cached charts file, fallback to the full index file.
   275  		// This is much slower but can happen after the caching feature is first
   276  		// installed but before the user  does a 'helm repo update' to generate the
   277  		// first cached charts file.
   278  		path = filepath.Join(settings.RepositoryCache, helmpath.CacheIndexFile(repoName))
   279  		if indexFile, err := repo.LoadIndexFile(path); err == nil {
   280  			for name := range indexFile.Entries {
   281  				fullName := fmt.Sprintf("%s/%s", repoName, name)
   282  				if strings.HasPrefix(fullName, prefix) {
   283  					charts = append(charts, fullName)
   284  				}
   285  			}
   286  			return charts
   287  		}
   288  	}
   289  
   290  	return []string{}
   291  }
   292  
   293  // Provide dynamic auto-completion for commands that operate on charts (e.g., helm show)
   294  // When true, the includeFiles argument indicates that completion should include local files (e.g., local charts)
   295  func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.ShellCompDirective) {
   296  	cobra.CompDebugln(fmt.Sprintf("compListCharts with toComplete %s", toComplete), settings.Debug)
   297  
   298  	noSpace := false
   299  	noFile := false
   300  	var completions []string
   301  
   302  	// First check completions for repos
   303  	repos := compListRepos("", nil)
   304  	for _, repoInfo := range repos {
   305  		// Split name from description
   306  		repoInfo := strings.Split(repoInfo, "\t")
   307  		repo := repoInfo[0]
   308  		repoDesc := ""
   309  		if len(repoInfo) > 1 {
   310  			repoDesc = repoInfo[1]
   311  		}
   312  		repoWithSlash := fmt.Sprintf("%s/", repo)
   313  		if strings.HasPrefix(toComplete, repoWithSlash) {
   314  			// Must complete with charts within the specified repo.
   315  			// Don't filter on toComplete to allow for shell fuzzy matching
   316  			completions = append(completions, compListChartsOfRepo(repo, "")...)
   317  			noSpace = false
   318  			break
   319  		} else if strings.HasPrefix(repo, toComplete) {
   320  			// Must complete the repo name with the slash, followed by the description
   321  			completions = append(completions, fmt.Sprintf("%s\t%s", repoWithSlash, repoDesc))
   322  			noSpace = true
   323  		}
   324  	}
   325  	cobra.CompDebugln(fmt.Sprintf("Completions after repos: %v", completions), settings.Debug)
   326  
   327  	// Now handle completions for url prefixes
   328  	for _, url := range []string{"oci://\tChart OCI prefix", "https://\tChart URL prefix", "http://\tChart URL prefix", "file://\tChart local URL prefix"} {
   329  		if strings.HasPrefix(toComplete, url) {
   330  			// The user already put in the full url prefix; we don't have
   331  			// anything to add, but make sure the shell does not default
   332  			// to file completion since we could be returning an empty array.
   333  			noFile = true
   334  			noSpace = true
   335  		} else if strings.HasPrefix(url, toComplete) {
   336  			// We are completing a url prefix
   337  			completions = append(completions, url)
   338  			noSpace = true
   339  		}
   340  	}
   341  	cobra.CompDebugln(fmt.Sprintf("Completions after urls: %v", completions), settings.Debug)
   342  
   343  	// Finally, provide file completion if we need to.
   344  	// We only do this if:
   345  	// 1- There are other completions found (if there are no completions,
   346  	//    the shell will do file completion itself)
   347  	// 2- If there is some input from the user (or else we will end up
   348  	//    listing the entire content of the current directory which will
   349  	//    be too many choices for the user to find the real repos)
   350  	if includeFiles && len(completions) > 0 && len(toComplete) > 0 {
   351  		if files, err := os.ReadDir("."); err == nil {
   352  			for _, file := range files {
   353  				if strings.HasPrefix(file.Name(), toComplete) {
   354  					// We are completing a file prefix
   355  					completions = append(completions, file.Name())
   356  				}
   357  			}
   358  		}
   359  	}
   360  	cobra.CompDebugln(fmt.Sprintf("Completions after files: %v", completions), settings.Debug)
   361  
   362  	// If the user didn't provide any input to completion,
   363  	// we provide a hint that a path can also be used
   364  	if includeFiles && len(toComplete) == 0 {
   365  		completions = append(completions, "./\tRelative path prefix to local chart", "/\tAbsolute path prefix to local chart")
   366  	}
   367  	cobra.CompDebugln(fmt.Sprintf("Completions after checking empty input: %v", completions), settings.Debug)
   368  
   369  	directive := cobra.ShellCompDirectiveDefault
   370  	if noFile {
   371  		directive = directive | cobra.ShellCompDirectiveNoFileComp
   372  	}
   373  	if noSpace {
   374  		directive = directive | cobra.ShellCompDirectiveNoSpace
   375  	}
   376  	if !includeFiles {
   377  		// If we should not include files in the completions,
   378  		// we should disable file completion
   379  		directive = directive | cobra.ShellCompDirectiveNoFileComp
   380  	}
   381  	return completions, directive
   382  }