2022-08-25 08:13:03 +00:00
package registry
2023-02-28 21:54:20 +00:00
import (
"context"
"net/http"
"strconv"
"strings"
2023-09-13 15:41:45 +00:00
"github.com/containerd/log"
2022-08-25 08:13:03 +00:00
"github.com/docker/distribution/registry/client/auth"
2023-02-28 21:54:20 +00:00
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/errdefs"
"github.com/pkg/errors"
)
var acceptedSearchFilterTags = map [ string ] bool {
2023-04-12 11:33:38 +00:00
"is-automated" : true , // Deprecated: the "is_automated" field is deprecated and will always be false in the future.
2023-02-28 21:54:20 +00:00
"is-official" : true ,
"stars" : true ,
}
// Search queries the public registry for repositories matching the specified
// search term and filters.
2023-03-01 00:25:15 +00:00
func ( s * Service ) Search ( ctx context . Context , searchFilters filters . Args , term string , limit int , authConfig * registry . AuthConfig , headers map [ string ] [ ] string ) ( [ ] registry . SearchResult , error ) {
2023-02-28 21:54:20 +00:00
if err := searchFilters . Validate ( acceptedSearchFilterTags ) ; err != nil {
return nil , err
}
2023-04-12 11:33:38 +00:00
// 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".
2023-02-28 21:54:20 +00:00
isAutomated , err := searchFilters . GetBoolOrDefault ( "is-automated" , false )
if err != nil {
return nil , err
}
isOfficial , err := searchFilters . GetBoolOrDefault ( "is-official" , false )
if err != nil {
return nil , err
}
hasStarFilter := 0
if searchFilters . Contains ( "stars" ) {
hasStars := searchFilters . Get ( "stars" )
for _ , hasStar := range hasStars {
iHasStar , err := strconv . Atoi ( hasStar )
if err != nil {
return nil , errdefs . InvalidParameter ( errors . Wrapf ( err , "invalid filter 'stars=%s'" , hasStar ) )
}
if iHasStar > hasStarFilter {
hasStarFilter = iHasStar
}
}
}
2023-04-12 11:33:38 +00:00
// TODO(thaJeztah): the "is-automated" field is deprecated. Reset the field for the next release (v26.0.0) if any "true" values are present.
2023-03-21 13:40:33 +00:00
unfilteredResult , err := s . searchUnfiltered ( ctx , term , limit , authConfig , headers )
2023-02-28 21:54:20 +00:00
if err != nil {
return nil , err
}
filteredResults := [ ] registry . SearchResult { }
for _ , result := range unfilteredResult . Results {
if searchFilters . Contains ( "is-automated" ) {
2023-04-12 11:33:38 +00:00
if isAutomated != result . IsAutomated { //nolint:staticcheck // ignore SA1019 for old API versions.
2023-02-28 21:54:20 +00:00
continue
}
}
if searchFilters . Contains ( "is-official" ) {
if isOfficial != result . IsOfficial {
continue
}
}
if searchFilters . Contains ( "stars" ) {
if result . StarCount < hasStarFilter {
continue
}
}
filteredResults = append ( filteredResults , result )
}
return filteredResults , nil
}
2023-03-21 13:40:33 +00:00
func ( s * Service ) searchUnfiltered ( ctx context . Context , term string , limit int , authConfig * registry . AuthConfig , headers http . Header ) ( * registry . SearchResults , error ) {
2023-02-28 21:54:20 +00:00
// TODO Use ctx when searching for repositories
if hasScheme ( term ) {
return nil , invalidParamf ( "invalid repository name: repository name (%s) should not have a scheme" , term )
}
indexName , remoteName := splitReposSearchTerm ( term )
// Search is a long-running operation, just lock s.config to avoid block others.
s . mu . RLock ( )
index , err := newIndexInfo ( s . config , indexName )
s . mu . RUnlock ( )
if err != nil {
return nil , err
}
if index . Official {
// If pull "library/foo", it's stored locally under "foo"
remoteName = strings . TrimPrefix ( remoteName , "library/" )
}
2023-03-21 13:40:33 +00:00
endpoint , err := newV1Endpoint ( index , headers )
2023-02-28 21:54:20 +00:00
if err != nil {
return nil , err
}
var client * http . Client
if authConfig != nil && authConfig . IdentityToken != "" && authConfig . Username != "" {
creds := NewStaticCredentialStore ( authConfig )
scopes := [ ] auth . Scope {
auth . RegistryScope {
Name : "catalog" ,
Actions : [ ] string { "search" } ,
} ,
}
2023-03-21 13:40:33 +00:00
// TODO(thaJeztah); is there a reason not to include other headers here? (originally added in 19d48f0b8ba59eea9f2cac4ad1c7977712a6b7ac)
modifiers := Headers ( headers . Get ( "User-Agent" ) , nil )
2023-02-28 21:54:20 +00:00
v2Client , err := v2AuthHTTPClient ( endpoint . URL , endpoint . client . Transport , modifiers , creds , scopes )
if err != nil {
return nil , err
}
// Copy non transport http client features
v2Client . Timeout = endpoint . client . Timeout
v2Client . CheckRedirect = endpoint . client . CheckRedirect
v2Client . Jar = endpoint . client . Jar
2023-06-23 00:33:17 +00:00
log . G ( ctx ) . Debugf ( "using v2 client for search to %s" , endpoint . URL )
2023-02-28 21:54:20 +00:00
client = v2Client
} else {
client = endpoint . client
if err := authorizeClient ( client , authConfig , endpoint ) ; err != nil {
return nil , err
}
}
return newSession ( client , endpoint ) . searchRepositories ( remoteName , limit )
}
2022-08-25 08:13:03 +00:00
// splitReposSearchTerm breaks a search term into an index name and remote name
func splitReposSearchTerm ( reposName string ) ( string , string ) {
nameParts := strings . SplitN ( reposName , "/" , 2 )
if len ( nameParts ) == 1 || ( ! strings . Contains ( nameParts [ 0 ] , "." ) &&
! strings . Contains ( nameParts [ 0 ] , ":" ) && nameParts [ 0 ] != "localhost" ) {
// This is a Docker Hub repository (ex: samalba/hipache or ubuntu),
// use the default Docker Hub registry (docker.io)
return IndexName , reposName
}
return nameParts [ 0 ] , nameParts [ 1 ]
}
// ParseSearchIndexInfo will use repository name to get back an indexInfo.
//
// TODO(thaJeztah) this function is only used by the CLI, and used to get
// information of the registry (to provide credentials if needed). We should
// move this function (or equivalent) to the CLI, as it's doing too much just
// for that.
func ParseSearchIndexInfo ( reposName string ) ( * registry . IndexInfo , error ) {
indexName , _ := splitReposSearchTerm ( reposName )
return newIndexInfo ( emptyServiceConfig , indexName )
}