github.com/tonistiigi/docker@v0.10.1-0.20240229224939-974013b0dc6a/registry/search.go (about)

     1  package registry
     2  
     3  import (
     4  	"context"
     5  	"net/http"
     6  	"strconv"
     7  	"strings"
     8  
     9  	"github.com/containerd/log"
    10  	"github.com/docker/distribution/registry/client/auth"
    11  	"github.com/docker/docker/api/types/filters"
    12  	"github.com/docker/docker/api/types/registry"
    13  	"github.com/docker/docker/errdefs"
    14  	"github.com/pkg/errors"
    15  )
    16  
    17  var acceptedSearchFilterTags = map[string]bool{
    18  	"is-automated": true, // Deprecated: the "is_automated" field is deprecated and will always be false in the future.
    19  	"is-official":  true,
    20  	"stars":        true,
    21  }
    22  
    23  // Search queries the public registry for repositories matching the specified
    24  // search term and filters.
    25  func (s *Service) Search(ctx context.Context, searchFilters filters.Args, term string, limit int, authConfig *registry.AuthConfig, headers map[string][]string) ([]registry.SearchResult, error) {
    26  	if err := searchFilters.Validate(acceptedSearchFilterTags); err != nil {
    27  		return nil, err
    28  	}
    29  
    30  	// TODO(thaJeztah): the "is-automated" field is deprecated; reset the field for the next release (v26.0.0). Return early when using "is-automated=true", and ignore "is-automated=false".
    31  	isAutomated, err := searchFilters.GetBoolOrDefault("is-automated", false)
    32  	if err != nil {
    33  		return nil, err
    34  	}
    35  	isOfficial, err := searchFilters.GetBoolOrDefault("is-official", false)
    36  	if err != nil {
    37  		return nil, err
    38  	}
    39  
    40  	hasStarFilter := 0
    41  	if searchFilters.Contains("stars") {
    42  		hasStars := searchFilters.Get("stars")
    43  		for _, hasStar := range hasStars {
    44  			iHasStar, err := strconv.Atoi(hasStar)
    45  			if err != nil {
    46  				return nil, errdefs.InvalidParameter(errors.Wrapf(err, "invalid filter 'stars=%s'", hasStar))
    47  			}
    48  			if iHasStar > hasStarFilter {
    49  				hasStarFilter = iHasStar
    50  			}
    51  		}
    52  	}
    53  
    54  	// TODO(thaJeztah): the "is-automated" field is deprecated. Reset the field for the next release (v26.0.0) if any "true" values are present.
    55  	unfilteredResult, err := s.searchUnfiltered(ctx, term, limit, authConfig, headers)
    56  	if err != nil {
    57  		return nil, err
    58  	}
    59  
    60  	filteredResults := []registry.SearchResult{}
    61  	for _, result := range unfilteredResult.Results {
    62  		if searchFilters.Contains("is-automated") {
    63  			if isAutomated != result.IsAutomated { //nolint:staticcheck // ignore SA1019 for old API versions.
    64  				continue
    65  			}
    66  		}
    67  		if searchFilters.Contains("is-official") {
    68  			if isOfficial != result.IsOfficial {
    69  				continue
    70  			}
    71  		}
    72  		if searchFilters.Contains("stars") {
    73  			if result.StarCount < hasStarFilter {
    74  				continue
    75  			}
    76  		}
    77  		filteredResults = append(filteredResults, result)
    78  	}
    79  
    80  	return filteredResults, nil
    81  }
    82  
    83  func (s *Service) searchUnfiltered(ctx context.Context, term string, limit int, authConfig *registry.AuthConfig, headers http.Header) (*registry.SearchResults, error) {
    84  	// TODO Use ctx when searching for repositories
    85  	if hasScheme(term) {
    86  		return nil, invalidParamf("invalid repository name: repository name (%s) should not have a scheme", term)
    87  	}
    88  
    89  	indexName, remoteName := splitReposSearchTerm(term)
    90  
    91  	// Search is a long-running operation, just lock s.config to avoid block others.
    92  	s.mu.RLock()
    93  	index, err := newIndexInfo(s.config, indexName)
    94  	s.mu.RUnlock()
    95  
    96  	if err != nil {
    97  		return nil, err
    98  	}
    99  	if index.Official {
   100  		// If pull "library/foo", it's stored locally under "foo"
   101  		remoteName = strings.TrimPrefix(remoteName, "library/")
   102  	}
   103  
   104  	endpoint, err := newV1Endpoint(index, headers)
   105  	if err != nil {
   106  		return nil, err
   107  	}
   108  
   109  	var client *http.Client
   110  	if authConfig != nil && authConfig.IdentityToken != "" && authConfig.Username != "" {
   111  		creds := NewStaticCredentialStore(authConfig)
   112  		scopes := []auth.Scope{
   113  			auth.RegistryScope{
   114  				Name:    "catalog",
   115  				Actions: []string{"search"},
   116  			},
   117  		}
   118  
   119  		// TODO(thaJeztah); is there a reason not to include other headers here? (originally added in 19d48f0b8ba59eea9f2cac4ad1c7977712a6b7ac)
   120  		modifiers := Headers(headers.Get("User-Agent"), nil)
   121  		v2Client, err := v2AuthHTTPClient(endpoint.URL, endpoint.client.Transport, modifiers, creds, scopes)
   122  		if err != nil {
   123  			return nil, err
   124  		}
   125  		// Copy non transport http client features
   126  		v2Client.Timeout = endpoint.client.Timeout
   127  		v2Client.CheckRedirect = endpoint.client.CheckRedirect
   128  		v2Client.Jar = endpoint.client.Jar
   129  
   130  		log.G(ctx).Debugf("using v2 client for search to %s", endpoint.URL)
   131  		client = v2Client
   132  	} else {
   133  		client = endpoint.client
   134  		if err := authorizeClient(client, authConfig, endpoint); err != nil {
   135  			return nil, err
   136  		}
   137  	}
   138  
   139  	return newSession(client, endpoint).searchRepositories(remoteName, limit)
   140  }
   141  
   142  // splitReposSearchTerm breaks a search term into an index name and remote name
   143  func splitReposSearchTerm(reposName string) (string, string) {
   144  	nameParts := strings.SplitN(reposName, "/", 2)
   145  	if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") &&
   146  		!strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") {
   147  		// This is a Docker Hub repository (ex: samalba/hipache or ubuntu),
   148  		// use the default Docker Hub registry (docker.io)
   149  		return IndexName, reposName
   150  	}
   151  	return nameParts[0], nameParts[1]
   152  }
   153  
   154  // ParseSearchIndexInfo will use repository name to get back an indexInfo.
   155  //
   156  // TODO(thaJeztah) this function is only used by the CLI, and used to get
   157  // information of the registry (to provide credentials if needed). We should
   158  // move this function (or equivalent) to the CLI, as it's doing too much just
   159  // for that.
   160  func ParseSearchIndexInfo(reposName string) (*registry.IndexInfo, error) {
   161  	indexName, _ := splitReposSearchTerm(reposName)
   162  	return newIndexInfo(emptyServiceConfig, indexName)
   163  }