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 }