github.com/containers/libpod@v1.9.4-0.20220419124438-4284fd425507/libpod/image/search.go (about)

     1  package image
     2  
     3  import (
     4  	"context"
     5  	"strconv"
     6  	"strings"
     7  	"sync"
     8  
     9  	"github.com/containers/image/v5/docker"
    10  	"github.com/containers/image/v5/types"
    11  	sysreg "github.com/containers/libpod/pkg/registries"
    12  	"github.com/pkg/errors"
    13  	"github.com/sirupsen/logrus"
    14  	"golang.org/x/sync/semaphore"
    15  )
    16  
    17  const (
    18  	descriptionTruncLength = 44
    19  	maxQueries             = 25
    20  	maxParallelSearches    = int64(6)
    21  )
    22  
    23  // SearchResult is holding image-search related data.
    24  type SearchResult struct {
    25  	// Index is the image index (e.g., "docker.io" or "quay.io")
    26  	Index string
    27  	// Name is the canoncical name of the image (e.g., "docker.io/library/alpine").
    28  	Name string
    29  	// Description of the image.
    30  	Description string
    31  	// Stars is the number of stars of the image.
    32  	Stars int
    33  	// Official indicates if it's an official image.
    34  	Official string
    35  	// Automated indicates if the image was created by an automated build.
    36  	Automated string
    37  }
    38  
    39  // SearchOptions are used to control the behaviour of SearchImages.
    40  type SearchOptions struct {
    41  	// Filter allows to filter the results.
    42  	Filter SearchFilter
    43  	// Limit limits the number of queries per index (default: 25). Must be
    44  	// greater than 0 to overwrite the default value.
    45  	Limit int
    46  	// NoTrunc avoids the output to be truncated.
    47  	NoTrunc bool
    48  	// Authfile is the path to the authentication file.
    49  	Authfile string
    50  	// InsecureSkipTLSVerify allows to skip TLS verification.
    51  	InsecureSkipTLSVerify types.OptionalBool
    52  }
    53  
    54  // SearchFilter allows filtering the results of SearchImages.
    55  type SearchFilter struct {
    56  	// Stars describes the minimal amount of starts of an image.
    57  	Stars int
    58  	// IsAutomated decides if only images from automated builds are displayed.
    59  	IsAutomated types.OptionalBool
    60  	// IsOfficial decides if only official images are displayed.
    61  	IsOfficial types.OptionalBool
    62  }
    63  
    64  // SearchImages searches images based on term and the specified SearchOptions
    65  // in all registries.
    66  func SearchImages(term string, options SearchOptions) ([]SearchResult, error) {
    67  	// Check if search term has a registry in it
    68  	registry, err := sysreg.GetRegistry(term)
    69  	if err != nil {
    70  		return nil, errors.Wrapf(err, "error getting registry from %q", term)
    71  	}
    72  	if registry != "" {
    73  		term = term[len(registry)+1:]
    74  	}
    75  
    76  	registries, err := getRegistries(registry)
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  
    81  	// searchOutputData is used as a return value for searching in parallel.
    82  	type searchOutputData struct {
    83  		data []SearchResult
    84  		err  error
    85  	}
    86  
    87  	// Let's follow Firefox by limiting parallel downloads to 6.
    88  	sem := semaphore.NewWeighted(maxParallelSearches)
    89  	wg := sync.WaitGroup{}
    90  	wg.Add(len(registries))
    91  	data := make([]searchOutputData, len(registries))
    92  
    93  	searchImageInRegistryHelper := func(index int, registry string) {
    94  		defer sem.Release(1)
    95  		defer wg.Done()
    96  		searchOutput, err := searchImageInRegistry(term, registry, options)
    97  		data[index] = searchOutputData{data: searchOutput, err: err}
    98  	}
    99  
   100  	ctx := context.Background()
   101  	for i := range registries {
   102  		if err := sem.Acquire(ctx, 1); err != nil {
   103  			return nil, err
   104  		}
   105  		go searchImageInRegistryHelper(i, registries[i])
   106  	}
   107  
   108  	wg.Wait()
   109  	results := []SearchResult{}
   110  	for _, d := range data {
   111  		if d.err != nil {
   112  			return nil, d.err
   113  		}
   114  		results = append(results, d.data...)
   115  	}
   116  	return results, nil
   117  }
   118  
   119  // getRegistries returns the list of registries to search, depending on an optional registry specification
   120  func getRegistries(registry string) ([]string, error) {
   121  	var registries []string
   122  	if registry != "" {
   123  		registries = append(registries, registry)
   124  	} else {
   125  		var err error
   126  		registries, err = sysreg.GetRegistries()
   127  		if err != nil {
   128  			return nil, errors.Wrapf(err, "error getting registries to search")
   129  		}
   130  	}
   131  	return registries, nil
   132  }
   133  
   134  func searchImageInRegistry(term string, registry string, options SearchOptions) ([]SearchResult, error) {
   135  	// Max number of queries by default is 25
   136  	limit := maxQueries
   137  	if options.Limit > 0 {
   138  		limit = options.Limit
   139  	}
   140  
   141  	sc := GetSystemContext("", options.Authfile, false)
   142  	sc.DockerInsecureSkipTLSVerify = options.InsecureSkipTLSVerify
   143  	// FIXME: Set this more globally.  Probably no reason not to have it in
   144  	// every types.SystemContext, and to compute the value just once in one
   145  	// place.
   146  	sc.SystemRegistriesConfPath = sysreg.SystemRegistriesConfPath()
   147  	results, err := docker.SearchRegistry(context.TODO(), sc, registry, term, limit)
   148  	if err != nil {
   149  		logrus.Errorf("error searching registry %q: %v", registry, err)
   150  		return []SearchResult{}, nil
   151  	}
   152  	index := registry
   153  	arr := strings.Split(registry, ".")
   154  	if len(arr) > 2 {
   155  		index = strings.Join(arr[len(arr)-2:], ".")
   156  	}
   157  
   158  	// limit is the number of results to output
   159  	// if the total number of results is less than the limit, output all
   160  	// if the limit has been set by the user, output those number of queries
   161  	limit = maxQueries
   162  	if len(results) < limit {
   163  		limit = len(results)
   164  	}
   165  	if options.Limit != 0 {
   166  		limit = len(results)
   167  		if options.Limit < len(results) {
   168  			limit = options.Limit
   169  		}
   170  	}
   171  
   172  	paramsArr := []SearchResult{}
   173  	for i := 0; i < limit; i++ {
   174  		// Check whether query matches filters
   175  		if !(options.Filter.matchesAutomatedFilter(results[i]) && options.Filter.matchesOfficialFilter(results[i]) && options.Filter.matchesStarFilter(results[i])) {
   176  			continue
   177  		}
   178  		official := ""
   179  		if results[i].IsOfficial {
   180  			official = "[OK]"
   181  		}
   182  		automated := ""
   183  		if results[i].IsAutomated {
   184  			automated = "[OK]"
   185  		}
   186  		description := strings.Replace(results[i].Description, "\n", " ", -1)
   187  		if len(description) > 44 && !options.NoTrunc {
   188  			description = description[:descriptionTruncLength] + "..."
   189  		}
   190  		name := registry + "/" + results[i].Name
   191  		if index == "docker.io" && !strings.Contains(results[i].Name, "/") {
   192  			name = index + "/library/" + results[i].Name
   193  		}
   194  		params := SearchResult{
   195  			Index:       index,
   196  			Name:        name,
   197  			Description: description,
   198  			Official:    official,
   199  			Automated:   automated,
   200  			Stars:       results[i].StarCount,
   201  		}
   202  		paramsArr = append(paramsArr, params)
   203  	}
   204  	return paramsArr, nil
   205  }
   206  
   207  // ParseSearchFilter turns the filter into a SearchFilter that can be used for
   208  // searching images.
   209  func ParseSearchFilter(filter []string) (*SearchFilter, error) {
   210  	sFilter := new(SearchFilter)
   211  	for _, f := range filter {
   212  		arr := strings.Split(f, "=")
   213  		switch arr[0] {
   214  		case "stars":
   215  			if len(arr) < 2 {
   216  				return nil, errors.Errorf("invalid `stars` filter %q, should be stars=<value>", filter)
   217  			}
   218  			stars, err := strconv.Atoi(arr[1])
   219  			if err != nil {
   220  				return nil, errors.Wrapf(err, "incorrect value type for stars filter")
   221  			}
   222  			sFilter.Stars = stars
   223  		case "is-automated":
   224  			if len(arr) == 2 && arr[1] == "false" {
   225  				sFilter.IsAutomated = types.OptionalBoolFalse
   226  			} else {
   227  				sFilter.IsAutomated = types.OptionalBoolTrue
   228  			}
   229  		case "is-official":
   230  			if len(arr) == 2 && arr[1] == "false" {
   231  				sFilter.IsOfficial = types.OptionalBoolFalse
   232  			} else {
   233  				sFilter.IsOfficial = types.OptionalBoolTrue
   234  			}
   235  		default:
   236  			return nil, errors.Errorf("invalid filter type %q", f)
   237  		}
   238  	}
   239  	return sFilter, nil
   240  }
   241  
   242  func (f *SearchFilter) matchesStarFilter(result docker.SearchResult) bool {
   243  	return result.StarCount >= f.Stars
   244  }
   245  
   246  func (f *SearchFilter) matchesAutomatedFilter(result docker.SearchResult) bool {
   247  	if f.IsAutomated != types.OptionalBoolUndefined {
   248  		return result.IsAutomated == (f.IsAutomated == types.OptionalBoolTrue)
   249  	}
   250  	return true
   251  }
   252  
   253  func (f *SearchFilter) matchesOfficialFilter(result docker.SearchResult) bool {
   254  	if f.IsOfficial != types.OptionalBoolUndefined {
   255  		return result.IsOfficial == (f.IsOfficial == types.OptionalBoolTrue)
   256  	}
   257  	return true
   258  }