search.go 5.4 KB

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