Merge pull request #45086 from corhere/search-in-registry-service
Move filtered registry search out of the image service
This commit is contained in:
commit
1c84f63a40
18 changed files with 230 additions and 253 deletions
|
@ -39,5 +39,8 @@ type importExportBackend interface {
|
|||
type registryBackend interface {
|
||||
PullImage(ctx context.Context, image, tag string, platform *specs.Platform, metaHeaders map[string][]string, authConfig *registry.AuthConfig, outStream io.Writer) error
|
||||
PushImage(ctx context.Context, image, tag string, metaHeaders map[string][]string, authConfig *registry.AuthConfig, outStream io.Writer) error
|
||||
SearchRegistryForImages(ctx context.Context, searchFilters filters.Args, term string, limit int, authConfig *registry.AuthConfig, metaHeaders map[string][]string) (*registry.SearchResults, error)
|
||||
}
|
||||
|
||||
type Searcher interface {
|
||||
Search(ctx context.Context, searchFilters filters.Args, term string, limit int, authConfig *registry.AuthConfig, metaHeaders map[string][]string) ([]registry.SearchResult, error)
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
// imageRouter is a router to talk with the image controller
|
||||
type imageRouter struct {
|
||||
backend Backend
|
||||
searcher Searcher
|
||||
referenceBackend reference.Store
|
||||
imageStore image.Store
|
||||
layerStore layer.Store
|
||||
|
@ -17,9 +18,10 @@ type imageRouter struct {
|
|||
}
|
||||
|
||||
// NewRouter initializes a new image router
|
||||
func NewRouter(backend Backend, referenceBackend reference.Store, imageStore image.Store, layerStore layer.Store) router.Router {
|
||||
func NewRouter(backend Backend, searcher Searcher, referenceBackend reference.Store, imageStore image.Store, layerStore layer.Store) router.Router {
|
||||
ir := &imageRouter{
|
||||
backend: backend,
|
||||
searcher: searcher,
|
||||
referenceBackend: referenceBackend,
|
||||
imageStore: imageStore,
|
||||
layerStore: layerStore,
|
||||
|
|
|
@ -415,11 +415,11 @@ func (ir *imageRouter) getImagesSearch(ctx context.Context, w http.ResponseWrite
|
|||
// For a search it is not an error if no auth was given. Ignore invalid
|
||||
// AuthConfig to increase compatibility with the existing API.
|
||||
authConfig, _ := registry.DecodeAuthConfig(r.Header.Get(registry.AuthHeader))
|
||||
query, err := ir.backend.SearchRegistryForImages(ctx, searchFilters, r.Form.Get("term"), limit, authConfig, headers)
|
||||
res, err := ir.searcher.Search(ctx, searchFilters, r.Form.Get("term"), limit, authConfig, headers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return httputils.WriteJSON(w, http.StatusOK, query.Results)
|
||||
return httputils.WriteJSON(w, http.StatusOK, res)
|
||||
}
|
||||
|
||||
func (ir *imageRouter) postImagesPrune(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
|
||||
|
|
|
@ -511,6 +511,7 @@ func initRouter(opts routerOptions) {
|
|||
container.NewRouter(opts.daemon, decoder, opts.daemon.RawSysInfo().CgroupUnified),
|
||||
image.NewRouter(
|
||||
opts.daemon.ImageService(),
|
||||
opts.daemon.RegistryService(),
|
||||
opts.daemon.ReferenceStore,
|
||||
opts.daemon.ImageService().DistributionServices().ImageStore,
|
||||
opts.daemon.ImageService().DistributionServices().LayerStore,
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
package containerd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/errdefs"
|
||||
)
|
||||
|
||||
// SearchRegistryForImages queries the registry for images matching
|
||||
// term. authConfig is used to login.
|
||||
//
|
||||
// TODO: this could be implemented in a registry service instead of the image
|
||||
// service.
|
||||
func (i *ImageService) SearchRegistryForImages(ctx context.Context, searchFilters filters.Args, term string, limit int, authConfig *registry.AuthConfig, metaHeaders map[string][]string) (*registry.SearchResults, error) {
|
||||
return nil, errdefs.NotImplemented(errors.New("not implemented"))
|
||||
}
|
|
@ -23,7 +23,7 @@ func (i *ImageService) newResolverFromAuthConfig(authConfig *registrytypes.AuthC
|
|||
}), tracker
|
||||
}
|
||||
|
||||
func hostsWrapper(hostsFn docker.RegistryHosts, authConfig *registrytypes.AuthConfig, regService registry.Service) docker.RegistryHosts {
|
||||
func hostsWrapper(hostsFn docker.RegistryHosts, authConfig *registrytypes.AuthConfig, regService RegistryConfigProvider) docker.RegistryHosts {
|
||||
return func(n string) ([]docker.RegistryHost, error) {
|
||||
hosts, err := hostsFn(n)
|
||||
if err != nil {
|
||||
|
|
|
@ -14,7 +14,6 @@ import (
|
|||
"github.com/docker/docker/errdefs"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/layer"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/opencontainers/image-spec/identity"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
@ -27,15 +26,19 @@ type ImageService struct {
|
|||
containers container.Store
|
||||
snapshotter string
|
||||
registryHosts RegistryHostsProvider
|
||||
registryService registry.Service
|
||||
registryService RegistryConfigProvider
|
||||
}
|
||||
|
||||
type RegistryHostsProvider interface {
|
||||
RegistryHosts() docker.RegistryHosts
|
||||
}
|
||||
|
||||
type RegistryConfigProvider interface {
|
||||
IsInsecureRegistry(host string) bool
|
||||
}
|
||||
|
||||
// NewService creates a new ImageService.
|
||||
func NewService(c *containerd.Client, containers container.Store, snapshotter string, hostsProvider RegistryHostsProvider, registry registry.Service) *ImageService {
|
||||
func NewService(c *containerd.Client, containers container.Store, snapshotter string, hostsProvider RegistryHostsProvider, registry RegistryConfigProvider) *ImageService {
|
||||
return &ImageService{
|
||||
client: c,
|
||||
containers: containers,
|
||||
|
|
|
@ -82,7 +82,7 @@ type Daemon struct {
|
|||
configStore *config.Config
|
||||
statsCollector *stats.Collector
|
||||
defaultLogConfig containertypes.LogConfig
|
||||
registryService registry.Service
|
||||
registryService *registry.Service
|
||||
EventsService *events.Events
|
||||
netController *libnetwork.Controller
|
||||
volumes *volumesservice.VolumesService
|
||||
|
@ -1459,6 +1459,11 @@ func (daemon *Daemon) ImageService() ImageService {
|
|||
return daemon.imageService
|
||||
}
|
||||
|
||||
// RegistryService returns the Daemon's RegistryService
|
||||
func (daemon *Daemon) RegistryService() *registry.Service {
|
||||
return daemon.registryService
|
||||
}
|
||||
|
||||
// BuilderBackend returns the backend used by builder
|
||||
func (daemon *Daemon) BuilderBackend() builder.Backend {
|
||||
return struct {
|
||||
|
|
|
@ -73,7 +73,6 @@ type ImageService interface {
|
|||
// Other
|
||||
|
||||
GetRepository(ctx context.Context, ref reference.Named, authConfig *registry.AuthConfig) (distribution.Repository, error)
|
||||
SearchRegistryForImages(ctx context.Context, searchFilters filters.Args, term string, limit int, authConfig *registry.AuthConfig, headers map[string][]string) (*registry.SearchResults, error)
|
||||
DistributionServices() images.DistributionServices
|
||||
Children(id image.ID) []image.ID
|
||||
Cleanup() error
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
package images // import "github.com/docker/docker/daemon/images"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/dockerversion"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var acceptedSearchFilterTags = map[string]bool{
|
||||
"is-automated": true,
|
||||
"is-official": true,
|
||||
"stars": true,
|
||||
}
|
||||
|
||||
// SearchRegistryForImages queries the registry for images matching
|
||||
// term. authConfig is used to login.
|
||||
//
|
||||
// TODO: this could be implemented in a registry service instead of the image
|
||||
// service.
|
||||
func (i *ImageService) SearchRegistryForImages(ctx context.Context, searchFilters filters.Args, term string, limit int,
|
||||
authConfig *registry.AuthConfig,
|
||||
headers map[string][]string) (*registry.SearchResults, error) {
|
||||
if err := searchFilters.Validate(acceptedSearchFilterTags); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unfilteredResult, err := i.registryService.Search(ctx, term, limit, authConfig, dockerversion.DockerUserAgent(ctx), headers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filteredResults := []registry.SearchResult{}
|
||||
for _, result := range unfilteredResult.Results {
|
||||
if searchFilters.Contains("is-automated") {
|
||||
if isAutomated != result.IsAutomated {
|
||||
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 ®istry.SearchResults{
|
||||
Query: unfilteredResult.Query,
|
||||
NumResults: len(filteredResults),
|
||||
Results: filteredResults,
|
||||
}, nil
|
||||
}
|
|
@ -8,12 +8,12 @@ import (
|
|||
"github.com/containerd/containerd/leases"
|
||||
"github.com/docker/docker/container"
|
||||
daemonevents "github.com/docker/docker/daemon/events"
|
||||
"github.com/docker/docker/distribution"
|
||||
"github.com/docker/docker/distribution/metadata"
|
||||
"github.com/docker/docker/distribution/xfer"
|
||||
"github.com/docker/docker/image"
|
||||
"github.com/docker/docker/layer"
|
||||
dockerreference "github.com/docker/docker/reference"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
@ -39,7 +39,7 @@ type ImageServiceConfig struct {
|
|||
MaxConcurrentUploads int
|
||||
MaxDownloadAttempts int
|
||||
ReferenceStore dockerreference.Store
|
||||
RegistryService registry.Service
|
||||
RegistryService distribution.RegistryResolver
|
||||
ContentStore content.Store
|
||||
Leases leases.Manager
|
||||
ContentNamespace string
|
||||
|
@ -73,7 +73,7 @@ type ImageService struct {
|
|||
layerStore layer.Store
|
||||
pruneRunning int32
|
||||
referenceStore dockerreference.Store
|
||||
registryService registry.Service
|
||||
registryService distribution.RegistryResolver
|
||||
uploadManager *xfer.LayerUploadManager
|
||||
leases leases.Manager
|
||||
content content.Store
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/manifest/schema2"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/distribution/metadata"
|
||||
"github.com/docker/docker/distribution/xfer"
|
||||
|
@ -35,7 +36,7 @@ type Config struct {
|
|||
ProgressOutput progress.Output
|
||||
// RegistryService is the registry service to use for TLS configuration
|
||||
// and endpoint lookup.
|
||||
RegistryService registrypkg.Service
|
||||
RegistryService RegistryResolver
|
||||
// ImageEventLogger notifies events for a given image
|
||||
ImageEventLogger func(id, name, action string)
|
||||
// MetadataStore is the storage backend for distribution-specific
|
||||
|
@ -75,6 +76,13 @@ type ImagePushConfig struct {
|
|||
UploadManager *xfer.LayerUploadManager
|
||||
}
|
||||
|
||||
// RegistryResolver is used for TLS configuration and endpoint lookup.
|
||||
type RegistryResolver interface {
|
||||
LookupPushEndpoints(hostname string) (endpoints []registrypkg.APIEndpoint, err error)
|
||||
LookupPullEndpoints(hostname string) (endpoints []registrypkg.APIEndpoint, err error)
|
||||
ResolveRepository(name reference.Named) (*registrypkg.RepositoryInfo, error)
|
||||
}
|
||||
|
||||
// ImageConfigStore handles storing and getting image configurations
|
||||
// by digest. Allows getting an image configurations rootfs from the
|
||||
// configuration.
|
||||
|
|
|
@ -520,7 +520,7 @@ func TestMirrorEndpointLookup(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := defaultService{config: cfg}
|
||||
s := Service{config: cfg}
|
||||
|
||||
imageName, err := reference.WithName(IndexName + "/test/image")
|
||||
if err != nil {
|
||||
|
|
139
registry/search.go
Normal file
139
registry/search.go
Normal file
|
@ -0,0 +1,139 @@
|
|||
package registry // import "github.com/docker/docker/registry"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/dockerversion"
|
||||
"github.com/docker/docker/errdefs"
|
||||
|
||||
"github.com/docker/distribution/registry/client/auth"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var acceptedSearchFilterTags = map[string]bool{
|
||||
"is-automated": true,
|
||||
"is-official": true,
|
||||
"stars": true,
|
||||
}
|
||||
|
||||
// Search queries the public registry for repositories matching the specified
|
||||
// search term and filters.
|
||||
func (s *Service) Search(ctx context.Context, searchFilters filters.Args, term string, limit int, authConfig *registry.AuthConfig, headers map[string][]string) ([]registry.SearchResult, error) {
|
||||
if err := searchFilters.Validate(acceptedSearchFilterTags); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unfilteredResult, err := s.searchUnfiltered(ctx, term, limit, authConfig, dockerversion.DockerUserAgent(ctx), headers)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filteredResults := []registry.SearchResult{}
|
||||
for _, result := range unfilteredResult.Results {
|
||||
if searchFilters.Contains("is-automated") {
|
||||
if isAutomated != result.IsAutomated {
|
||||
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
|
||||
}
|
||||
|
||||
func (s *Service) searchUnfiltered(ctx context.Context, term string, limit int, authConfig *registry.AuthConfig, userAgent string, headers map[string][]string) (*registry.SearchResults, error) {
|
||||
// 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/")
|
||||
}
|
||||
|
||||
endpoint, err := newV1Endpoint(index, userAgent, headers)
|
||||
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"},
|
||||
},
|
||||
}
|
||||
|
||||
modifiers := Headers(userAgent, nil)
|
||||
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
|
||||
|
||||
logrus.Debugf("using v2 client for search to %s", endpoint.URL)
|
||||
client = v2Client
|
||||
} else {
|
||||
client = endpoint.client
|
||||
if err := authorizeClient(client, authConfig, endpoint); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return newSession(client, endpoint).searchRepositories(remoteName, limit)
|
||||
}
|
|
@ -1,44 +1,26 @@
|
|||
package images // import "github.com/docker/docker/daemon/images"
|
||||
package registry // import "github.com/docker/docker/registry"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/errdefs"
|
||||
registrypkg "github.com/docker/docker/registry"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
type fakeService struct {
|
||||
registrypkg.Service
|
||||
shouldReturnError bool
|
||||
|
||||
term string
|
||||
results []registry.SearchResult
|
||||
}
|
||||
|
||||
func (s *fakeService) Search(ctx context.Context, term string, limit int, authConfig *registry.AuthConfig, userAgent string, headers map[string][]string) (*registry.SearchResults, error) {
|
||||
if s.shouldReturnError {
|
||||
return nil, errdefs.Unknown(errors.New("search unknown error"))
|
||||
}
|
||||
return ®istry.SearchResults{
|
||||
Query: s.term,
|
||||
NumResults: len(s.results),
|
||||
Results: s.results,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func TestSearchRegistryForImagesErrors(t *testing.T) {
|
||||
func TestSearchErrors(t *testing.T) {
|
||||
errorCases := []struct {
|
||||
filtersArgs filters.Args
|
||||
shouldReturnError bool
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
expectedError: "search unknown error",
|
||||
expectedError: "Unexpected status code 500",
|
||||
shouldReturnError: true,
|
||||
},
|
||||
{
|
||||
|
@ -82,12 +64,20 @@ func TestSearchRegistryForImagesErrors(t *testing.T) {
|
|||
for _, tc := range errorCases {
|
||||
tc := tc
|
||||
t.Run(tc.expectedError, func(t *testing.T) {
|
||||
daemon := &ImageService{
|
||||
registryService: &fakeService{
|
||||
shouldReturnError: tc.shouldReturnError,
|
||||
},
|
||||
}
|
||||
_, err := daemon.SearchRegistryForImages(context.Background(), tc.filtersArgs, "term", 0, nil, map[string][]string{})
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !tc.shouldReturnError {
|
||||
t.Errorf("unexpected HTTP request")
|
||||
}
|
||||
http.Error(w, "no search for you", http.StatusInternalServerError)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
// Construct the search term by cutting the 'http://' prefix off srv.URL.
|
||||
term := srv.URL[7:] + "/term"
|
||||
|
||||
reg, err := NewService(ServiceOptions{})
|
||||
assert.NilError(t, err)
|
||||
_, err = reg.Search(context.Background(), tc.filtersArgs, term, 0, nil, map[string][]string{})
|
||||
assert.ErrorContains(t, err, tc.expectedError)
|
||||
if tc.shouldReturnError {
|
||||
assert.Check(t, errdefs.IsUnknown(err), "got: %T: %v", err, err)
|
||||
|
@ -98,8 +88,8 @@ func TestSearchRegistryForImagesErrors(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSearchRegistryForImages(t *testing.T) {
|
||||
term := "term"
|
||||
func TestSearch(t *testing.T) {
|
||||
const term = "term"
|
||||
successCases := []struct {
|
||||
name string
|
||||
filtersArgs filters.Args
|
||||
|
@ -348,17 +338,24 @@ func TestSearchRegistryForImages(t *testing.T) {
|
|||
for _, tc := range successCases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
daemon := &ImageService{
|
||||
registryService: &fakeService{
|
||||
term: term,
|
||||
results: tc.registryResults,
|
||||
},
|
||||
}
|
||||
results, err := daemon.SearchRegistryForImages(context.Background(), tc.filtersArgs, term, 0, nil, map[string][]string{})
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-type", "application/json")
|
||||
json.NewEncoder(w).Encode(registry.SearchResults{
|
||||
Query: term,
|
||||
NumResults: len(tc.registryResults),
|
||||
Results: tc.registryResults,
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
// Construct the search term by cutting the 'http://' prefix off srv.URL.
|
||||
searchTerm := srv.URL[7:] + "/" + term
|
||||
|
||||
reg, err := NewService(ServiceOptions{})
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, results.Query, term)
|
||||
assert.Equal(t, results.NumResults, len(tc.expectedResults))
|
||||
assert.DeepEqual(t, results.Results, tc.expectedResults)
|
||||
results, err := reg.Search(context.Background(), tc.filtersArgs, searchTerm, 0, nil, map[string][]string{})
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, results, tc.expectedResults)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -3,56 +3,40 @@ package registry // import "github.com/docker/docker/registry"
|
|||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/distribution/registry/client/auth"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/errdefs"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// Service is the interface defining what a registry service should implement.
|
||||
type Service interface {
|
||||
Auth(ctx context.Context, authConfig *registry.AuthConfig, userAgent string) (status, token string, err error)
|
||||
LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error)
|
||||
LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error)
|
||||
ResolveRepository(name reference.Named) (*RepositoryInfo, error)
|
||||
Search(ctx context.Context, term string, limit int, authConfig *registry.AuthConfig, userAgent string, headers map[string][]string) (*registry.SearchResults, error)
|
||||
ServiceConfig() *registry.ServiceConfig
|
||||
LoadAllowNondistributableArtifacts([]string) error
|
||||
LoadMirrors([]string) error
|
||||
LoadInsecureRegistries([]string) error
|
||||
IsInsecureRegistry(string) bool
|
||||
}
|
||||
|
||||
// defaultService is a registry service. It tracks configuration data such as a list
|
||||
// Service is a registry service. It tracks configuration data such as a list
|
||||
// of mirrors.
|
||||
type defaultService struct {
|
||||
type Service struct {
|
||||
config *serviceConfig
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewService returns a new instance of defaultService ready to be
|
||||
// installed into an engine.
|
||||
func NewService(options ServiceOptions) (Service, error) {
|
||||
func NewService(options ServiceOptions) (*Service, error) {
|
||||
config, err := newServiceConfig(options)
|
||||
|
||||
return &defaultService{config: config}, err
|
||||
return &Service{config: config}, err
|
||||
}
|
||||
|
||||
// ServiceConfig returns a copy of the public registry service's configuration.
|
||||
func (s *defaultService) ServiceConfig() *registry.ServiceConfig {
|
||||
func (s *Service) ServiceConfig() *registry.ServiceConfig {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.config.copy()
|
||||
}
|
||||
|
||||
// LoadAllowNondistributableArtifacts loads allow-nondistributable-artifacts registries for Service.
|
||||
func (s *defaultService) LoadAllowNondistributableArtifacts(registries []string) error {
|
||||
func (s *Service) LoadAllowNondistributableArtifacts(registries []string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
|
@ -60,7 +44,7 @@ func (s *defaultService) LoadAllowNondistributableArtifacts(registries []string)
|
|||
}
|
||||
|
||||
// LoadMirrors loads registry mirrors for Service
|
||||
func (s *defaultService) LoadMirrors(mirrors []string) error {
|
||||
func (s *Service) LoadMirrors(mirrors []string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
|
@ -68,7 +52,7 @@ func (s *defaultService) LoadMirrors(mirrors []string) error {
|
|||
}
|
||||
|
||||
// LoadInsecureRegistries loads insecure registries for Service
|
||||
func (s *defaultService) LoadInsecureRegistries(registries []string) error {
|
||||
func (s *Service) LoadInsecureRegistries(registries []string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
|
@ -78,7 +62,7 @@ func (s *defaultService) LoadInsecureRegistries(registries []string) error {
|
|||
// Auth contacts the public registry with the provided credentials,
|
||||
// and returns OK if authentication was successful.
|
||||
// It can be used to verify the validity of a client's credentials.
|
||||
func (s *defaultService) Auth(ctx context.Context, authConfig *registry.AuthConfig, userAgent string) (status, token string, err error) {
|
||||
func (s *Service) Auth(ctx context.Context, authConfig *registry.AuthConfig, userAgent string) (status, token string, err error) {
|
||||
// TODO Use ctx when searching for repositories
|
||||
var registryHostName = IndexHostname
|
||||
|
||||
|
@ -129,69 +113,9 @@ func splitReposSearchTerm(reposName string) (string, string) {
|
|||
return nameParts[0], nameParts[1]
|
||||
}
|
||||
|
||||
// Search queries the public registry for images matching the specified
|
||||
// search terms, and returns the results.
|
||||
func (s *defaultService) Search(ctx context.Context, term string, limit int, authConfig *registry.AuthConfig, userAgent string, headers map[string][]string) (*registry.SearchResults, error) {
|
||||
// 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/")
|
||||
}
|
||||
|
||||
endpoint, err := newV1Endpoint(index, userAgent, headers)
|
||||
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"},
|
||||
},
|
||||
}
|
||||
|
||||
modifiers := Headers(userAgent, nil)
|
||||
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
|
||||
|
||||
logrus.Debugf("using v2 client for search to %s", endpoint.URL)
|
||||
client = v2Client
|
||||
} else {
|
||||
client = endpoint.client
|
||||
if err := authorizeClient(client, authConfig, endpoint); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return newSession(client, endpoint).searchRepositories(remoteName, limit)
|
||||
}
|
||||
|
||||
// ResolveRepository splits a repository name into its components
|
||||
// and configuration of the associated registry.
|
||||
func (s *defaultService) ResolveRepository(name reference.Named) (*RepositoryInfo, error) {
|
||||
func (s *Service) ResolveRepository(name reference.Named) (*RepositoryInfo, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return newRepositoryInfo(s.config, name)
|
||||
|
@ -210,7 +134,7 @@ type APIEndpoint struct {
|
|||
|
||||
// LookupPullEndpoints creates a list of v2 endpoints to try to pull from, in order of preference.
|
||||
// It gives preference to mirrors over the actual registry, and HTTPS over plain HTTP.
|
||||
func (s *defaultService) LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
|
||||
func (s *Service) LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
|
@ -219,7 +143,7 @@ func (s *defaultService) LookupPullEndpoints(hostname string) (endpoints []APIEn
|
|||
|
||||
// LookupPushEndpoints creates a list of v2 endpoints to try to push to, in order of preference.
|
||||
// It gives preference to HTTPS over plain HTTP. Mirrors are not included.
|
||||
func (s *defaultService) LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
|
||||
func (s *Service) LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
|
@ -236,7 +160,7 @@ func (s *defaultService) LookupPushEndpoints(hostname string) (endpoints []APIEn
|
|||
|
||||
// IsInsecureRegistry returns true if the registry at given host is configured as
|
||||
// insecure registry.
|
||||
func (s *defaultService) IsInsecureRegistry(host string) bool {
|
||||
func (s *Service) IsInsecureRegistry(host string) bool {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return !s.config.isSecureIndex(host)
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"github.com/docker/go-connections/tlsconfig"
|
||||
)
|
||||
|
||||
func (s *defaultService) lookupV2Endpoints(hostname string) (endpoints []APIEndpoint, err error) {
|
||||
func (s *Service) lookupV2Endpoints(hostname string) (endpoints []APIEndpoint, err error) {
|
||||
ana := s.config.allowNondistributableArtifacts(hostname)
|
||||
|
||||
if hostname == DefaultNamespace || hostname == IndexHostname {
|
||||
|
|
|
@ -206,10 +206,10 @@ func (r *session) searchRepositories(term string, limit int) (*registry.SearchRe
|
|||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, &jsonmessage.JSONError{
|
||||
return nil, errdefs.Unknown(&jsonmessage.JSONError{
|
||||
Message: fmt.Sprintf("Unexpected status code %d", res.StatusCode),
|
||||
Code: res.StatusCode,
|
||||
}
|
||||
})
|
||||
}
|
||||
result := new(registry.SearchResults)
|
||||
return result, errors.Wrap(json.NewDecoder(res.Body).Decode(result), "error decoding registry search results")
|
||||
|
|
Loading…
Add table
Reference in a new issue