automation-tests/common/libimage/search.go

306 lines
9.2 KiB
Go

//go:build !remote
package libimage
import (
"context"
"fmt"
"strings"
"sync"
"github.com/containers/common/libimage/filter"
registryTransport "github.com/containers/image/v5/docker"
"github.com/containers/image/v5/pkg/sysregistriesv2"
"github.com/containers/image/v5/transports/alltransports"
"github.com/containers/image/v5/types"
"github.com/hashicorp/go-multierror"
"github.com/sirupsen/logrus"
"golang.org/x/sync/semaphore"
)
const (
searchTruncLength = 44
searchMaxQueries = 25
// Let's follow Firefox by limiting parallel downloads to 6. We do the
// same when pulling images in c/image.
searchMaxParallel = int64(6)
)
// SearchResult is holding image-search related data.
type SearchResult struct {
// Index is the image index (e.g., "docker.io" or "quay.io")
Index string
// Name is the canonical name of the image (e.g., "docker.io/library/alpine").
Name string
// Description of the image.
Description string
// Stars is the number of stars of the image.
Stars int
// Official indicates if it's an official image.
Official string
// Automated indicates if the image was created by an automated build.
Automated string
// Tag is the image tag
Tag string
}
// SearchOptions customize searching images.
type SearchOptions struct {
// Filter allows to filter the results.
Filter filter.SearchFilter
// Limit limits the number of queries per index (default: 25). Must be
// greater than 0 to overwrite the default value.
Limit int
// NoTrunc avoids the output to be truncated.
NoTrunc bool
// Authfile is the path to the authentication file.
Authfile string
// Path to the certificates directory.
CertDirPath string
// Username to use when authenticating at a container registry.
Username string
// Password to use when authenticating at a container registry.
Password string
// Credentials is an alternative way to specify credentials in format
// "username[:password]". Cannot be used in combination with
// Username/Password.
Credentials string
// IdentityToken is used to authenticate the user and get
// an access token for the registry.
IdentityToken string `json:"identitytoken,omitempty"`
// InsecureSkipTLSVerify allows to skip TLS verification.
InsecureSkipTLSVerify types.OptionalBool
// ListTags returns the search result with available tags
ListTags bool
// Registries to search if the specified term does not include a
// registry. If set, the unqualified-search registries in
// containers-registries.conf(5) are ignored.
Registries []string
}
// Search searches term. If term includes a registry, only this registry will
// be used for searching. Otherwise, the unqualified-search registries in
// containers-registries.conf(5) or the ones specified in the options will be
// used.
func (r *Runtime) Search(ctx context.Context, term string, options *SearchOptions) ([]SearchResult, error) {
if options == nil {
options = &SearchOptions{}
}
var searchRegistries []string
// Try to extract a registry from the specified search term. We
// consider everything before the first slash to be the registry. Note
// that we cannot use the reference parser from the containers/image
// library as the search term may container arbitrary input such as
// wildcards. See bugzilla.redhat.com/show_bug.cgi?id=1846629.
spl := strings.SplitN(term, "/", 2)
switch {
case len(spl) > 1:
searchRegistries = []string{spl[0]}
term = spl[1]
case len(options.Registries) > 0:
searchRegistries = options.Registries
default:
regs, err := sysregistriesv2.UnqualifiedSearchRegistries(r.systemContextCopy())
if err != nil {
return nil, err
}
searchRegistries = regs
}
logrus.Debugf("Searching images matching term %s at the following registries %s", term, searchRegistries)
// searchOutputData is used as a return value for searching in parallel.
type searchOutputData struct {
data []SearchResult
err error
}
sem := semaphore.NewWeighted(searchMaxParallel)
wg := sync.WaitGroup{}
wg.Add(len(searchRegistries))
data := make([]searchOutputData, len(searchRegistries))
for i := range searchRegistries {
if err := sem.Acquire(ctx, 1); err != nil {
return nil, err
}
index := i
go func() {
defer sem.Release(1)
defer wg.Done()
searchOutput, err := r.searchImageInRegistry(ctx, term, searchRegistries[index], options)
data[index] = searchOutputData{data: searchOutput, err: err}
}()
}
wg.Wait()
results := []SearchResult{}
var multiErr error
for _, d := range data {
if d.err != nil {
multiErr = multierror.Append(multiErr, d.err)
continue
}
results = append(results, d.data...)
}
// Optimistically assume that one successfully searched registry
// includes what the user is looking for.
if len(results) > 0 {
return results, nil
}
return results, multiErr
}
func (r *Runtime) searchImageInRegistry(ctx context.Context, term, registry string, options *SearchOptions) ([]SearchResult, error) {
// Max number of queries by default is 25
limit := searchMaxQueries
if options.Limit > 0 {
limit = options.Limit
}
sys := r.systemContextCopy()
if options.InsecureSkipTLSVerify != types.OptionalBoolUndefined {
sys.DockerInsecureSkipTLSVerify = options.InsecureSkipTLSVerify
}
if options.Authfile != "" {
sys.AuthFilePath = options.Authfile
}
if options.CertDirPath != "" {
sys.DockerCertPath = options.CertDirPath
}
dockerAuthConfig, err := getDockerAuthConfig(options.Username, options.Password, options.Credentials, options.IdentityToken)
if err != nil {
return nil, err
}
if dockerAuthConfig != nil {
sys.DockerAuthConfig = dockerAuthConfig
}
if options.ListTags {
results, err := searchRepositoryTags(ctx, sys, registry, term, options)
if err != nil {
return []SearchResult{}, err
}
return results, nil
}
results, err := registryTransport.SearchRegistry(ctx, sys, registry, term, limit)
if err != nil {
return []SearchResult{}, err
}
index := registry
arr := strings.Split(registry, ".")
if len(arr) > 2 {
index = strings.Join(arr[len(arr)-2:], ".")
}
// limit is the number of results to output
// if the total number of results is less than the limit, output all
// if the limit has been set by the user, output those number of queries
limit = searchMaxQueries
if len(results) < limit {
limit = len(results)
}
if options.Limit != 0 {
limit = len(results)
if options.Limit < len(results) {
limit = options.Limit
}
}
paramsArr := []SearchResult{}
for i := range limit {
// Check whether query matches filters
if !filterMatchesAutomatedFilter(&options.Filter, results[i]) || !filterMatchesOfficialFilter(&options.Filter, results[i]) || !filterMatchesStarFilter(&options.Filter, results[i]) {
continue
}
official := ""
if results[i].IsOfficial {
official = "[OK]"
}
automated := ""
if results[i].IsAutomated {
automated = "[OK]"
}
description := strings.ReplaceAll(results[i].Description, "\n", " ")
if len(description) > 44 && !options.NoTrunc {
description = description[:searchTruncLength] + "..."
}
name := registry + "/" + results[i].Name
if index == "docker.io" && !strings.Contains(results[i].Name, "/") {
name = index + "/library/" + results[i].Name
}
params := SearchResult{
Index: registry,
Name: name,
Description: description,
Official: official,
Automated: automated,
Stars: results[i].StarCount,
}
paramsArr = append(paramsArr, params)
}
return paramsArr, nil
}
func searchRepositoryTags(ctx context.Context, sys *types.SystemContext, registry, term string, options *SearchOptions) ([]SearchResult, error) {
dockerPrefix := "docker://"
imageRef, err := alltransports.ParseImageName(fmt.Sprintf("%s/%s", registry, term))
if err == nil && imageRef.Transport().Name() != registryTransport.Transport.Name() {
return nil, fmt.Errorf("reference %q must be a docker reference", term)
} else if err != nil {
imageRef, err = alltransports.ParseImageName(fmt.Sprintf("%s%s", dockerPrefix, fmt.Sprintf("%s/%s", registry, term)))
if err != nil {
return nil, fmt.Errorf("reference %q must be a docker reference", term)
}
}
tags, err := registryTransport.GetRepositoryTags(ctx, sys, imageRef)
if err != nil {
return nil, fmt.Errorf("getting repository tags: %v", err)
}
limit := searchMaxQueries
if len(tags) < limit {
limit = len(tags)
}
if options.Limit != 0 {
limit = len(tags)
if options.Limit < limit {
limit = options.Limit
}
}
paramsArr := []SearchResult{}
for i := range limit {
params := SearchResult{
Name: imageRef.DockerReference().Name(),
Tag: tags[i],
Index: registry,
}
paramsArr = append(paramsArr, params)
}
return paramsArr, nil
}
func filterMatchesStarFilter(f *filter.SearchFilter, result registryTransport.SearchResult) bool {
return result.StarCount >= f.Stars
}
func filterMatchesAutomatedFilter(f *filter.SearchFilter, result registryTransport.SearchResult) bool {
if f.IsAutomated != types.OptionalBoolUndefined {
return result.IsAutomated == (f.IsAutomated == types.OptionalBoolTrue)
}
return true
}
func filterMatchesOfficialFilter(f *filter.SearchFilter, result registryTransport.SearchResult) bool {
if f.IsOfficial != types.OptionalBoolUndefined {
return result.IsOfficial == (f.IsOfficial == types.OptionalBoolTrue)
}
return true
}