github.com/containers/podman/v2@v2.2.2-0.20210501105131-c1e07d070c4c/libpod/image/search.go (about)

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