306 lines
9.2 KiB
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
|
|
}
|