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 }