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