2019-08-14 01:24:07 +08:00
/ *
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 .
* /
2025-02-24 23:11:54 +08:00
package cmd
2019-08-14 01:24:07 +08:00
import (
2020-01-16 08:01:54 +08:00
"bufio"
"bytes"
2024-11-16 11:07:40 +08:00
"errors"
2019-08-14 01:24:07 +08:00
"fmt"
"io"
2025-04-08 00:25:16 +08:00
"log/slog"
2021-07-08 21:14:06 +08:00
"os"
2019-08-23 14:31:50 +08:00
"path/filepath"
2019-08-14 01:24:07 +08:00
"strings"
2019-10-04 01:42:59 +08:00
"github.com/Masterminds/semver/v3"
2019-08-14 01:24:07 +08:00
"github.com/gosuri/uitable"
"github.com/spf13/cobra"
2024-12-27 05:33:51 +08:00
"helm.sh/helm/v4/pkg/cli/output"
2025-02-24 23:11:54 +08:00
"helm.sh/helm/v4/pkg/cmd/search"
2024-12-27 05:33:51 +08:00
"helm.sh/helm/v4/pkg/helmpath"
2025-08-31 21:04:48 +08:00
"helm.sh/helm/v4/pkg/repo/v1"
2019-08-14 01:24:07 +08:00
)
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 .
2019-10-25 05:35:48 +08:00
It will display the latest stable versions of the charts found . If you
specify the -- devel flag , the output will include pre - release versions .
If you want to search using a version constraint , use -- version .
Examples :
# Search for stable release versions matching the keyword "nginx"
$ helm search repo nginx
# Search for release versions matching the keyword "nginx" , including pre - release versions
$ helm search repo nginx -- devel
2019-10-31 01:36:20 +08:00
# Search for the latest stable release for nginx - ingress with a major version of 1
2019-10-25 05:35:48 +08:00
$ helm search repo nginx - ingress -- version ^ 1.0 .0
2019-10-24 22:10:47 +08:00
2019-08-14 01:24:07 +08:00
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 {
2023-03-06 22:54:53 +08:00
versions bool
regexp bool
devel bool
version string
maxColWidth uint
repoFile string
repoCacheDir string
outputFormat output . Format
failOnNoResult bool
2019-08-14 01:24:07 +08:00
}
func newSearchRepoCmd ( out io . Writer ) * cobra . Command {
o := & searchRepoOptions { }
cmd := & cobra . Command {
Use : "repo [keyword]" ,
2019-08-14 02:15:24 +08:00
Short : "search repositories for a keyword in charts" ,
2019-08-14 01:24:07 +08:00
Long : searchRepoDesc ,
2024-03-12 05:13:34 +08:00
RunE : func ( _ * cobra . Command , args [ ] string ) error {
2019-08-23 14:31:50 +08:00
o . repoFile = settings . RepositoryConfig
o . repoCacheDir = settings . RepositoryCache
2019-08-14 01:24:07 +08:00
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" )
2019-10-24 22:10:47 +08:00
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" )
2019-08-14 01:24:07 +08:00
f . StringVar ( & o . version , "version" , "" , "search using semantic versioning constraints on repositories you have added" )
2019-08-14 02:15:24 +08:00
f . UintVar ( & o . maxColWidth , "max-col-width" , 50 , "maximum column width for output table" )
2023-03-06 22:54:53 +08:00
f . BoolVar ( & o . failOnNoResult , "fail-on-no-result" , false , "search fails if no results are found" )
2019-09-26 02:20:47 +08:00
bindOutputFlag ( cmd , & o . outputFormat )
2019-08-14 01:24:07 +08:00
return cmd
}
func ( o * searchRepoOptions ) run ( out io . Writer , args [ ] string ) error {
2019-10-24 22:10:47 +08:00
o . setupSearchedVersion ( )
2020-02-17 14:21:13 +08:00
index , err := o . buildIndex ( )
2019-08-14 01:24:07 +08:00
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
}
2023-03-06 22:54:53 +08:00
return o . outputFormat . Write ( out , & repoSearchWriter { data , o . maxColWidth , o . failOnNoResult } )
2019-08-14 01:24:07 +08:00
}
2019-10-24 22:10:47 +08:00
func ( o * searchRepoOptions ) setupSearchedVersion ( ) {
2025-04-10 21:06:03 +08:00
slog . Debug ( "original chart version" , "version" , o . version )
2019-10-24 22:10:47 +08:00
if o . version != "" {
return
}
if o . devel { // search for releases and prereleases (alpha, beta, and release candidate releases).
2025-04-10 21:06:03 +08:00
slog . Debug ( "setting version to >0.0.0-0" )
2019-10-24 22:10:47 +08:00
o . version = ">0.0.0-0"
2024-09-13 05:33:46 +08:00
} else { // search only for stable releases, prerelease versions will be skipped
2025-04-10 21:06:03 +08:00
slog . Debug ( "setting version to >0.0.0" )
2019-10-24 22:10:47 +08:00
o . version = ">0.0.0"
}
}
2019-08-14 01:24:07 +08:00
func ( o * searchRepoOptions ) applyConstraint ( res [ ] * search . Result ) ( [ ] * search . Result , error ) {
2021-02-03 03:18:01 +08:00
if o . version == "" {
2019-08-14 01:24:07 +08:00
return res , nil
}
constraint , err := semver . NewConstraint ( o . version )
if err != nil {
2024-11-16 11:07:40 +08:00
return res , fmt . Errorf ( "an invalid version/constraint format: %w" , err )
2019-08-14 01:24:07 +08:00
}
data := res [ : 0 ]
foundNames := map [ string ] bool { }
for _ , r := range res {
2021-02-03 03:18:01 +08:00
// if not returning all versions and already have found a result,
// you're done!
if ! o . versions && foundNames [ r . Name ] {
2019-08-14 01:24:07 +08:00
continue
}
v , err := semver . NewVersion ( r . Chart . Version )
2020-10-22 15:12:53 +08:00
if err != nil {
2021-02-03 03:18:01 +08:00
continue
}
if constraint . Check ( v ) {
data = append ( data , r )
foundNames [ r . Name ] = true
2019-08-14 01:24:07 +08:00
}
}
return data , nil
}
2020-02-17 14:21:13 +08:00
func ( o * searchRepoOptions ) buildIndex ( ) ( * search . Index , error ) {
2019-08-14 01:24:07 +08:00
// Load the repositories.yaml
2019-08-23 14:31:50 +08:00
rf , err := repo . LoadFile ( o . repoFile )
2019-08-29 05:13:49 +08:00
if isNotExist ( err ) || len ( rf . Repositories ) == 0 {
return nil , errors . New ( "no repositories configured" )
2019-08-14 01:24:07 +08:00
}
i := search . NewIndex ( )
for _ , re := range rf . Repositories {
n := re . Name
2019-08-23 14:31:50 +08:00
f := filepath . Join ( o . repoCacheDir , helmpath . CacheIndexFile ( n ) )
2019-08-14 01:24:07 +08:00
ind , err := repo . LoadIndexFile ( f )
if err != nil {
2025-04-10 21:06:03 +08:00
slog . Warn ( "repo is corrupt or missing" , "repo" , n , slog . Any ( "error" , err ) )
2019-08-14 01:24:07 +08:00
continue
}
i . AddRepo ( n , ind , o . versions || len ( o . version ) > 0 )
}
return i , nil
}
2019-09-26 02:20:47 +08:00
type repoChartElement struct {
2019-11-05 06:19:48 +08:00
Name string ` json:"name" `
Version string ` json:"version" `
AppVersion string ` json:"app_version" `
Description string ` json:"description" `
2019-09-26 02:20:47 +08:00
}
type repoSearchWriter struct {
2023-03-06 22:54:53 +08:00
results [ ] * search . Result
columnWidth uint
failOnNoResult bool
2019-09-26 02:20:47 +08:00
}
func ( r * repoSearchWriter ) WriteTable ( out io . Writer ) error {
if len ( r . results ) == 0 {
2023-03-06 22:54:53 +08:00
// Fail if no results found and --fail-on-no-result is enabled
if r . failOnNoResult {
return fmt . Errorf ( "no results found" )
}
2019-09-26 02:20:47 +08:00
_ , err := out . Write ( [ ] byte ( "No results found\n" ) )
if err != nil {
return fmt . Errorf ( "unable to write results: %s" , err )
}
return nil
}
table := uitable . New ( )
table . MaxColWidth = r . columnWidth
table . AddRow ( "NAME" , "CHART VERSION" , "APP VERSION" , "DESCRIPTION" )
for _ , r := range r . results {
table . AddRow ( r . Name , r . Chart . Version , r . Chart . AppVersion , r . Chart . Description )
}
2019-10-08 00:03:29 +08:00
return output . EncodeTable ( out , table )
2019-09-26 02:20:47 +08:00
}
func ( r * repoSearchWriter ) WriteJSON ( out io . Writer ) error {
2019-10-08 00:03:29 +08:00
return r . encodeByFormat ( out , output . JSON )
2019-09-26 02:20:47 +08:00
}
func ( r * repoSearchWriter ) WriteYAML ( out io . Writer ) error {
2019-10-08 00:03:29 +08:00
return r . encodeByFormat ( out , output . YAML )
2019-09-26 02:20:47 +08:00
}
2019-10-08 00:03:29 +08:00
func ( r * repoSearchWriter ) encodeByFormat ( out io . Writer , format output . Format ) error {
2023-03-06 22:54:53 +08:00
// Fail if no results found and --fail-on-no-result is enabled
if len ( r . results ) == 0 && r . failOnNoResult {
return fmt . Errorf ( "no results found" )
}
2019-09-26 02:20:47 +08:00
// Initialize the array so no results returns an empty array instead of null
chartList := make ( [ ] repoChartElement , 0 , len ( r . results ) )
for _ , r := range r . results {
chartList = append ( chartList , repoChartElement { r . Name , r . Chart . Version , r . Chart . AppVersion , r . Chart . Description } )
}
switch format {
2019-10-08 00:03:29 +08:00
case output . JSON :
return output . EncodeJSON ( out , chartList )
case output . YAML :
return output . EncodeYAML ( out , chartList )
2019-09-26 02:20:47 +08:00
}
// Because this is a non-exported function and only called internally by
// WriteJSON and WriteYAML, we shouldn't get invalid types
return nil
}
2019-12-31 21:42:12 +08:00
// Provides the list of charts that are part of the specified repo, and that starts with 'prefix'.
func compListChartsOfRepo ( repoName string , prefix string ) [ ] string {
var charts [ ] string
2020-01-16 08:01:54 +08:00
path := filepath . Join ( settings . RepositoryCache , helmpath . CacheChartsFile ( repoName ) )
2023-03-22 21:31:16 +08:00
content , err := os . ReadFile ( path )
2020-01-16 08:01:54 +08:00
if err == nil {
scanner := bufio . NewScanner ( bytes . NewReader ( content ) )
for scanner . Scan ( ) {
fullName := fmt . Sprintf ( "%s/%s" , repoName , scanner . Text ( ) )
2019-12-31 21:42:12 +08:00
if strings . HasPrefix ( fullName , prefix ) {
charts = append ( charts , fullName )
}
}
2020-01-16 08:01:54 +08:00
return charts
}
if isNotExist ( err ) {
// If there is no cached charts file, fallback to the full index file.
// This is much slower but can happen after the caching feature is first
// installed but before the user does a 'helm repo update' to generate the
// first cached charts file.
path = filepath . Join ( settings . RepositoryCache , helmpath . CacheIndexFile ( repoName ) )
if indexFile , err := repo . LoadIndexFile ( path ) ; err == nil {
for name := range indexFile . Entries {
fullName := fmt . Sprintf ( "%s/%s" , repoName , name )
if strings . HasPrefix ( fullName , prefix ) {
charts = append ( charts , fullName )
}
}
return charts
}
2019-12-31 21:42:12 +08:00
}
2020-01-16 08:01:54 +08:00
return [ ] string { }
2019-12-31 21:42:12 +08:00
}
// Provide dynamic auto-completion for commands that operate on charts (e.g., helm show)
// When true, the includeFiles argument indicates that completion should include local files (e.g., local charts)
2020-04-12 02:06:56 +08:00
func compListCharts ( toComplete string , includeFiles bool ) ( [ ] string , cobra . ShellCompDirective ) {
cobra . CompDebugln ( fmt . Sprintf ( "compListCharts with toComplete %s" , toComplete ) , settings . Debug )
2019-12-31 21:42:12 +08:00
noSpace := false
noFile := false
var completions [ ] string
// First check completions for repos
2020-04-10 23:39:34 +08:00
repos := compListRepos ( "" , nil )
2020-06-29 06:52:05 +08:00
for _ , repoInfo := range repos {
// Split name from description
repoInfo := strings . Split ( repoInfo , "\t" )
repo := repoInfo [ 0 ]
repoDesc := ""
if len ( repoInfo ) > 1 {
repoDesc = repoInfo [ 1 ]
}
2019-12-31 21:42:12 +08:00
repoWithSlash := fmt . Sprintf ( "%s/" , repo )
if strings . HasPrefix ( toComplete , repoWithSlash ) {
2021-12-31 23:49:49 +08:00
// Must complete with charts within the specified repo.
// Don't filter on toComplete to allow for shell fuzzy matching
completions = append ( completions , compListChartsOfRepo ( repo , "" ) ... )
2019-12-31 21:42:12 +08:00
noSpace = false
break
} else if strings . HasPrefix ( repo , toComplete ) {
2020-06-29 06:52:05 +08:00
// Must complete the repo name with the slash, followed by the description
completions = append ( completions , fmt . Sprintf ( "%s\t%s" , repoWithSlash , repoDesc ) )
2019-12-31 21:42:12 +08:00
noSpace = true
}
}
2020-04-12 02:06:56 +08:00
cobra . CompDebugln ( fmt . Sprintf ( "Completions after repos: %v" , completions ) , settings . Debug )
2019-12-31 21:42:12 +08:00
// Now handle completions for url prefixes
2022-01-14 04:19:02 +08:00
for _ , url := range [ ] string { "oci://\tChart OCI prefix" , "https://\tChart URL prefix" , "http://\tChart URL prefix" , "file://\tChart local URL prefix" } {
2019-12-31 21:42:12 +08:00
if strings . HasPrefix ( toComplete , url ) {
// The user already put in the full url prefix; we don't have
// anything to add, but make sure the shell does not default
// to file completion since we could be returning an empty array.
noFile = true
noSpace = true
} else if strings . HasPrefix ( url , toComplete ) {
// We are completing a url prefix
completions = append ( completions , url )
noSpace = true
}
}
2020-04-12 02:06:56 +08:00
cobra . CompDebugln ( fmt . Sprintf ( "Completions after urls: %v" , completions ) , settings . Debug )
2019-12-31 21:42:12 +08:00
// Finally, provide file completion if we need to.
// We only do this if:
// 1- There are other completions found (if there are no completions,
// the shell will do file completion itself)
// 2- If there is some input from the user (or else we will end up
// listing the entire content of the current directory which will
// be too many choices for the user to find the real repos)
if includeFiles && len ( completions ) > 0 && len ( toComplete ) > 0 {
2021-07-08 21:14:06 +08:00
if files , err := os . ReadDir ( "." ) ; err == nil {
2019-12-31 21:42:12 +08:00
for _ , file := range files {
if strings . HasPrefix ( file . Name ( ) , toComplete ) {
// We are completing a file prefix
completions = append ( completions , file . Name ( ) )
}
}
}
}
2020-04-12 02:06:56 +08:00
cobra . CompDebugln ( fmt . Sprintf ( "Completions after files: %v" , completions ) , settings . Debug )
2019-12-31 21:42:12 +08:00
// If the user didn't provide any input to completion,
// we provide a hint that a path can also be used
if includeFiles && len ( toComplete ) == 0 {
2020-06-29 06:52:05 +08:00
completions = append ( completions , "./\tRelative path prefix to local chart" , "/\tAbsolute path prefix to local chart" )
2019-12-31 21:42:12 +08:00
}
2020-04-12 02:06:56 +08:00
cobra . CompDebugln ( fmt . Sprintf ( "Completions after checking empty input: %v" , completions ) , settings . Debug )
2019-12-31 21:42:12 +08:00
2020-04-12 02:06:56 +08:00
directive := cobra . ShellCompDirectiveDefault
2019-12-31 21:42:12 +08:00
if noFile {
2020-04-12 02:06:56 +08:00
directive = directive | cobra . ShellCompDirectiveNoFileComp
2019-12-31 21:42:12 +08:00
}
if noSpace {
2020-04-12 02:06:56 +08:00
directive = directive | cobra . ShellCompDirectiveNoSpace
2019-12-31 21:42:12 +08:00
}
2020-08-08 15:43:34 +08:00
if ! includeFiles {
// If we should not include files in the completions,
// we should disable file completion
directive = directive | cobra . ShellCompDirectiveNoFileComp
}
2019-12-31 21:42:12 +08:00
return completions , directive
}