Add Unit test to daemon.SearchRegistryForImages…

… and refactor a little bit some daemon on the way.

- Move `SearchRegistryForImages` to a new file (`daemon/search.go`) as
  `daemon.go` is getting pretty big.
- `registry.Service` is now an interface (allowing us to decouple it a
  little bit and thus unit test easily).
- Add some unit test for `SearchRegistryForImages`.
- Use UniqueExactMatch for search filters
- And use empty restore id for now in client.ContainerStart.

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
This commit is contained in:
Vincent Demeester 2016-05-21 16:00:28 +02:00
parent 5812b6927c
commit 636c276f67
No known key found for this signature in database
GPG Key ID: 083CC6FD6EB699A3
11 changed files with 493 additions and 110 deletions

View File

@ -234,7 +234,7 @@ func (cli *DockerCli) CmdRun(args ...string) error {
}
//start the container
if err := cli.client.ContainerStart(ctx, createResponse.ID); err != nil {
if err := cli.client.ContainerStart(ctx, createResponse.ID, ""); err != nil {
// If we have holdHijackedConnection, we should notify
// holdHijackedConnection we are going to exit and wait
// to avoid the terminal are not restored.

View File

@ -113,7 +113,7 @@ func (cli *DockerCli) CmdStart(args ...string) error {
})
// 3. Start the container.
if err := cli.client.ContainerStart(ctx, container); err != nil {
if err := cli.client.ContainerStart(ctx, container, ""); err != nil {
cancelFun()
<-cErr
return err
@ -147,7 +147,7 @@ func (cli *DockerCli) CmdStart(args ...string) error {
func (cli *DockerCli) startContainersWithoutAttachments(ctx context.Context, containers []string) error {
var failedContainers []string
for _, container := range containers {
if err := cli.client.ContainerStart(ctx, container); err != nil {
if err := cli.client.ContainerStart(ctx, container, ""); err != nil {
fmt.Fprintf(cli.err, "%s\n", err)
failedContainers = append(failedContainers, container)
} else {

View File

@ -15,7 +15,6 @@ import (
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"sync"
"syscall"
@ -31,7 +30,6 @@ import (
"github.com/docker/engine-api/types"
containertypes "github.com/docker/engine-api/types/container"
networktypes "github.com/docker/engine-api/types/network"
registrytypes "github.com/docker/engine-api/types/registry"
"github.com/docker/engine-api/types/strslice"
// register graph drivers
_ "github.com/docker/docker/daemon/graphdriver/register"
@ -63,7 +61,6 @@ import (
volumedrivers "github.com/docker/docker/volume/drivers"
"github.com/docker/docker/volume/local"
"github.com/docker/docker/volume/store"
"github.com/docker/engine-api/types/filters"
"github.com/docker/go-connections/nat"
"github.com/docker/libnetwork"
nwconfig "github.com/docker/libnetwork/config"
@ -93,7 +90,7 @@ type Daemon struct {
configStore *Config
statsCollector *statsCollector
defaultLogConfig containertypes.LogConfig
RegistryService *registry.Service
RegistryService registry.Service
EventsService *events.Events
netController libnetwork.NetworkController
volumes *store.VolumeStore
@ -614,7 +611,7 @@ func (daemon *Daemon) registerLink(parent, child *container.Container, alias str
// NewDaemon sets up everything for the daemon to be able to service
// requests from the webserver.
func NewDaemon(config *Config, registryService *registry.Service, containerdRemote libcontainerd.Remote) (daemon *Daemon, err error) {
func NewDaemon(config *Config, registryService registry.Service, containerdRemote libcontainerd.Remote) (daemon *Daemon, err error) {
setDefaultMtu(config)
// Ensure we have compatible and valid configuration options
@ -1144,88 +1141,7 @@ func configureVolumes(config *Config, rootUID, rootGID int) (*store.VolumeStore,
// AuthenticateToRegistry checks the validity of credentials in authConfig
func (daemon *Daemon) AuthenticateToRegistry(ctx context.Context, authConfig *types.AuthConfig) (string, string, error) {
return daemon.RegistryService.Auth(authConfig, dockerversion.DockerUserAgent(ctx))
}
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.
func (daemon *Daemon) SearchRegistryForImages(ctx context.Context, filtersArgs string, term string,
authConfig *types.AuthConfig,
headers map[string][]string) (*registrytypes.SearchResults, error) {
searchFilters, err := filters.FromParam(filtersArgs)
if err != nil {
return nil, err
}
if err := searchFilters.Validate(acceptedSearchFilterTags); err != nil {
return nil, err
}
unfilteredResult, err := daemon.RegistryService.Search(term, authConfig, dockerversion.DockerUserAgent(ctx), headers)
if err != nil {
return nil, err
}
var isAutomated, isOfficial bool
var hasStarFilter = 0
if searchFilters.Include("is-automated") {
if searchFilters.ExactMatch("is-automated", "true") {
isAutomated = true
} else if !searchFilters.ExactMatch("is-automated", "false") {
return nil, fmt.Errorf("Invalid filter 'is-automated=%s'", searchFilters.Get("is-automated"))
}
}
if searchFilters.Include("is-official") {
if searchFilters.ExactMatch("is-official", "true") {
isOfficial = true
} else if !searchFilters.ExactMatch("is-official", "false") {
return nil, fmt.Errorf("Invalid filter 'is-official=%s'", searchFilters.Get("is-official"))
}
}
if searchFilters.Include("stars") {
hasStars := searchFilters.Get("stars")
for _, hasStar := range hasStars {
iHasStar, err := strconv.Atoi(hasStar)
if err != nil {
return nil, fmt.Errorf("Invalid filter 'stars=%s'", hasStar)
}
if iHasStar > hasStarFilter {
hasStarFilter = iHasStar
}
}
}
filteredResults := []registrytypes.SearchResult{}
for _, result := range unfilteredResult.Results {
if searchFilters.Include("is-automated") {
if isAutomated != result.IsAutomated {
continue
}
}
if searchFilters.Include("is-official") {
if isOfficial != result.IsOfficial {
continue
}
}
if searchFilters.Include("stars") {
if result.StarCount < hasStarFilter {
continue
}
}
filteredResults = append(filteredResults, result)
}
return &registrytypes.SearchResults{
Query: unfilteredResult.Query,
NumResults: len(filteredResults),
Results: filteredResults,
}, nil
return daemon.RegistryService.Auth(ctx, authConfig, dockerversion.DockerUserAgent(ctx))
}
// IsShuttingDown tells whether the daemon is shutting down or not

94
daemon/search.go Normal file
View File

@ -0,0 +1,94 @@
package daemon
import (
"fmt"
"strconv"
"golang.org/x/net/context"
"github.com/docker/docker/dockerversion"
"github.com/docker/engine-api/types"
"github.com/docker/engine-api/types/filters"
registrytypes "github.com/docker/engine-api/types/registry"
)
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.
func (daemon *Daemon) SearchRegistryForImages(ctx context.Context, filtersArgs string, term string,
authConfig *types.AuthConfig,
headers map[string][]string) (*registrytypes.SearchResults, error) {
searchFilters, err := filters.FromParam(filtersArgs)
if err != nil {
return nil, err
}
if err := searchFilters.Validate(acceptedSearchFilterTags); err != nil {
return nil, err
}
unfilteredResult, err := daemon.RegistryService.Search(ctx, term, authConfig, dockerversion.DockerUserAgent(ctx), headers)
if err != nil {
return nil, err
}
var isAutomated, isOfficial bool
var hasStarFilter = 0
if searchFilters.Include("is-automated") {
if searchFilters.UniqueExactMatch("is-automated", "true") {
isAutomated = true
} else if !searchFilters.UniqueExactMatch("is-automated", "false") {
return nil, fmt.Errorf("Invalid filter 'is-automated=%s'", searchFilters.Get("is-automated"))
}
}
if searchFilters.Include("is-official") {
if searchFilters.UniqueExactMatch("is-official", "true") {
isOfficial = true
} else if !searchFilters.UniqueExactMatch("is-official", "false") {
return nil, fmt.Errorf("Invalid filter 'is-official=%s'", searchFilters.Get("is-official"))
}
}
if searchFilters.Include("stars") {
hasStars := searchFilters.Get("stars")
for _, hasStar := range hasStars {
iHasStar, err := strconv.Atoi(hasStar)
if err != nil {
return nil, fmt.Errorf("Invalid filter 'stars=%s'", hasStar)
}
if iHasStar > hasStarFilter {
hasStarFilter = iHasStar
}
}
}
filteredResults := []registrytypes.SearchResult{}
for _, result := range unfilteredResult.Results {
if searchFilters.Include("is-automated") {
if isAutomated != result.IsAutomated {
continue
}
}
if searchFilters.Include("is-official") {
if isOfficial != result.IsOfficial {
continue
}
}
if searchFilters.Include("stars") {
if result.StarCount < hasStarFilter {
continue
}
}
filteredResults = append(filteredResults, result)
}
return &registrytypes.SearchResults{
Query: unfilteredResult.Query,
NumResults: len(filteredResults),
Results: filteredResults,
}, nil
}

357
daemon/search_test.go Normal file
View File

@ -0,0 +1,357 @@
package daemon
import (
"fmt"
"strings"
"testing"
"golang.org/x/net/context"
"github.com/docker/docker/registry"
"github.com/docker/engine-api/types"
registrytypes "github.com/docker/engine-api/types/registry"
)
type FakeService struct {
registry.DefaultService
shouldReturnError bool
term string
results []registrytypes.SearchResult
}
func (s *FakeService) Search(ctx context.Context, term string, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) {
if s.shouldReturnError {
return nil, fmt.Errorf("Search unknown error")
}
return &registrytypes.SearchResults{
Query: s.term,
NumResults: len(s.results),
Results: s.results,
}, nil
}
func TestSearchRegistryForImagesErrors(t *testing.T) {
errorCases := []struct {
filtersArgs string
shouldReturnError bool
expectedError string
}{
{
expectedError: "Search unknown error",
shouldReturnError: true,
},
{
filtersArgs: "invalid json",
expectedError: "invalid character 'i' looking for beginning of value",
},
{
filtersArgs: `{"type":{"custom":true}}`,
expectedError: "Invalid filter 'type'",
},
{
filtersArgs: `{"is-automated":{"invalid":true}}`,
expectedError: "Invalid filter 'is-automated=[invalid]'",
},
{
filtersArgs: `{"is-automated":{"true":true,"false":true}}`,
expectedError: "Invalid filter 'is-automated",
},
{
filtersArgs: `{"is-official":{"invalid":true}}`,
expectedError: "Invalid filter 'is-official=[invalid]'",
},
{
filtersArgs: `{"is-official":{"true":true,"false":true}}`,
expectedError: "Invalid filter 'is-official",
},
{
filtersArgs: `{"stars":{"invalid":true}}`,
expectedError: "Invalid filter 'stars=invalid'",
},
{
filtersArgs: `{"stars":{"1":true,"invalid":true}}`,
expectedError: "Invalid filter 'stars=invalid'",
},
}
for index, e := range errorCases {
daemon := &Daemon{
RegistryService: &FakeService{
shouldReturnError: e.shouldReturnError,
},
}
_, err := daemon.SearchRegistryForImages(context.Background(), e.filtersArgs, "term", nil, map[string][]string{})
if err == nil {
t.Errorf("%d: expected an error, got nothing", index)
}
if !strings.Contains(err.Error(), e.expectedError) {
t.Errorf("%d: expected error to contain %s, got %s", index, e.expectedError, err.Error())
}
}
}
func TestSearchRegistryForImages(t *testing.T) {
term := "term"
successCases := []struct {
filtersArgs string
registryResults []registrytypes.SearchResult
expectedResults []registrytypes.SearchResult
}{
{
filtersArgs: "",
registryResults: []registrytypes.SearchResult{},
expectedResults: []registrytypes.SearchResult{},
},
{
filtersArgs: "",
registryResults: []registrytypes.SearchResult{
{
Name: "name",
Description: "description",
},
},
expectedResults: []registrytypes.SearchResult{
{
Name: "name",
Description: "description",
},
},
},
{
filtersArgs: `{"is-automated":{"true":true}}`,
registryResults: []registrytypes.SearchResult{
{
Name: "name",
Description: "description",
},
},
expectedResults: []registrytypes.SearchResult{},
},
{
filtersArgs: `{"is-automated":{"true":true}}`,
registryResults: []registrytypes.SearchResult{
{
Name: "name",
Description: "description",
IsAutomated: true,
},
},
expectedResults: []registrytypes.SearchResult{
{
Name: "name",
Description: "description",
IsAutomated: true,
},
},
},
{
filtersArgs: `{"is-automated":{"false":true}}`,
registryResults: []registrytypes.SearchResult{
{
Name: "name",
Description: "description",
IsAutomated: true,
},
},
expectedResults: []registrytypes.SearchResult{},
},
{
filtersArgs: `{"is-automated":{"false":true}}`,
registryResults: []registrytypes.SearchResult{
{
Name: "name",
Description: "description",
IsAutomated: false,
},
},
expectedResults: []registrytypes.SearchResult{
{
Name: "name",
Description: "description",
IsAutomated: false,
},
},
},
{
filtersArgs: `{"is-official":{"true":true}}`,
registryResults: []registrytypes.SearchResult{
{
Name: "name",
Description: "description",
},
},
expectedResults: []registrytypes.SearchResult{},
},
{
filtersArgs: `{"is-official":{"true":true}}`,
registryResults: []registrytypes.SearchResult{
{
Name: "name",
Description: "description",
IsOfficial: true,
},
},
expectedResults: []registrytypes.SearchResult{
{
Name: "name",
Description: "description",
IsOfficial: true,
},
},
},
{
filtersArgs: `{"is-official":{"false":true}}`,
registryResults: []registrytypes.SearchResult{
{
Name: "name",
Description: "description",
IsOfficial: true,
},
},
expectedResults: []registrytypes.SearchResult{},
},
{
filtersArgs: `{"is-official":{"false":true}}`,
registryResults: []registrytypes.SearchResult{
{
Name: "name",
Description: "description",
IsOfficial: false,
},
},
expectedResults: []registrytypes.SearchResult{
{
Name: "name",
Description: "description",
IsOfficial: false,
},
},
},
{
filtersArgs: `{"stars":{"0":true}}`,
registryResults: []registrytypes.SearchResult{
{
Name: "name",
Description: "description",
StarCount: 0,
},
},
expectedResults: []registrytypes.SearchResult{
{
Name: "name",
Description: "description",
StarCount: 0,
},
},
},
{
filtersArgs: `{"stars":{"1":true}}`,
registryResults: []registrytypes.SearchResult{
{
Name: "name",
Description: "description",
StarCount: 0,
},
},
expectedResults: []registrytypes.SearchResult{},
},
{
filtersArgs: `{"stars":{"1":true}}`,
registryResults: []registrytypes.SearchResult{
{
Name: "name0",
Description: "description0",
StarCount: 0,
},
{
Name: "name1",
Description: "description1",
StarCount: 1,
},
},
expectedResults: []registrytypes.SearchResult{
{
Name: "name1",
Description: "description1",
StarCount: 1,
},
},
},
{
filtersArgs: `{"stars":{"1":true}, "is-official":{"true":true}, "is-automated":{"true":true}}`,
registryResults: []registrytypes.SearchResult{
{
Name: "name0",
Description: "description0",
StarCount: 0,
IsOfficial: true,
IsAutomated: true,
},
{
Name: "name1",
Description: "description1",
StarCount: 1,
IsOfficial: false,
IsAutomated: true,
},
{
Name: "name2",
Description: "description2",
StarCount: 1,
IsOfficial: true,
IsAutomated: false,
},
{
Name: "name3",
Description: "description3",
StarCount: 2,
IsOfficial: true,
IsAutomated: true,
},
},
expectedResults: []registrytypes.SearchResult{
{
Name: "name3",
Description: "description3",
StarCount: 2,
IsOfficial: true,
IsAutomated: true,
},
},
},
}
for index, s := range successCases {
daemon := &Daemon{
RegistryService: &FakeService{
term: term,
results: s.registryResults,
},
}
results, err := daemon.SearchRegistryForImages(context.Background(), s.filtersArgs, term, nil, map[string][]string{})
if err != nil {
t.Errorf("%d: %v", index, err)
}
if results.Query != term {
t.Errorf("%d: expected Query to be %s, got %s", index, term, results.Query)
}
if results.NumResults != len(s.expectedResults) {
t.Errorf("%d: expected NumResults to be %d, got %d", index, len(s.expectedResults), results.NumResults)
}
for _, result := range results.Results {
found := false
for _, expectedResult := range s.expectedResults {
if expectedResult.Name == result.Name &&
expectedResult.Description == result.Description &&
expectedResult.IsAutomated == result.IsAutomated &&
expectedResult.IsOfficial == result.IsOfficial &&
expectedResult.StarCount == result.StarCount {
found = true
}
}
if !found {
t.Errorf("%d: expected results %v, got %v", index, s.expectedResults, results.Results)
}
}
}
}

View File

@ -27,7 +27,7 @@ type ImagePullConfig struct {
ProgressOutput progress.Output
// RegistryService is the registry service to use for TLS configuration
// and endpoint lookup.
RegistryService *registry.Service
RegistryService registry.Service
// ImageEventLogger notifies events for a given image
ImageEventLogger func(id, name, action string)
// MetadataStore is the storage backend for distribution-specific

View File

@ -31,7 +31,7 @@ type ImagePushConfig struct {
ProgressOutput progress.Output
// RegistryService is the registry service to use for TLS configuration
// and endpoint lookup.
RegistryService *registry.Service
RegistryService registry.Service
// ImageEventLogger notifies events for a given image
ImageEventLogger func(id, name, action string)
// MetadataStore is the storage backend for distribution-specific

View File

@ -661,7 +661,7 @@ func TestMirrorEndpointLookup(t *testing.T) {
}
return false
}
s := Service{config: makeServiceConfig([]string{"my.mirror"}, nil)}
s := DefaultService{config: makeServiceConfig([]string{"my.mirror"}, nil)}
imageName, err := reference.WithName(IndexName + "/test/image")
if err != nil {

View File

@ -7,35 +7,50 @@ import (
"net/url"
"strings"
"golang.org/x/net/context"
"github.com/Sirupsen/logrus"
"github.com/docker/docker/reference"
"github.com/docker/engine-api/types"
registrytypes "github.com/docker/engine-api/types/registry"
)
// Service is a registry service. It tracks configuration data such as a list
// Service is the interface defining what a registry service should implement.
type Service interface {
Auth(ctx context.Context, authConfig *types.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)
ResolveIndex(name string) (*registrytypes.IndexInfo, error)
Search(ctx context.Context, term string, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error)
ServiceConfig() *registrytypes.ServiceConfig
TLSConfig(hostname string) (*tls.Config, error)
}
// DefaultService is a registry service. It tracks configuration data such as a list
// of mirrors.
type Service struct {
type DefaultService struct {
config *serviceConfig
}
// NewService returns a new instance of Service ready to be
// NewService returns a new instance of DefaultService ready to be
// installed into an engine.
func NewService(options ServiceOptions) *Service {
return &Service{
func NewService(options ServiceOptions) *DefaultService {
return &DefaultService{
config: newServiceConfig(options),
}
}
// ServiceConfig returns the public registry service configuration.
func (s *Service) ServiceConfig() *registrytypes.ServiceConfig {
func (s *DefaultService) ServiceConfig() *registrytypes.ServiceConfig {
return &s.config.ServiceConfig
}
// 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 *Service) Auth(authConfig *types.AuthConfig, userAgent string) (status, token string, err error) {
func (s *DefaultService) Auth(ctx context.Context, authConfig *types.AuthConfig, userAgent string) (status, token string, err error) {
// TODO Use ctx when searching for repositories
serverAddress := authConfig.ServerAddress
if serverAddress == "" {
serverAddress = IndexServer
@ -93,7 +108,8 @@ func splitReposSearchTerm(reposName string) (string, string) {
// Search queries the public registry for images matching the specified
// search terms, and returns the results.
func (s *Service) Search(term string, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) {
func (s *DefaultService) Search(ctx context.Context, term string, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) {
// TODO Use ctx when searching for repositories
if err := validateNoScheme(term); err != nil {
return nil, err
}
@ -130,12 +146,12 @@ func (s *Service) Search(term string, authConfig *types.AuthConfig, userAgent st
// ResolveRepository splits a repository name into its components
// and configuration of the associated registry.
func (s *Service) ResolveRepository(name reference.Named) (*RepositoryInfo, error) {
func (s *DefaultService) ResolveRepository(name reference.Named) (*RepositoryInfo, error) {
return newRepositoryInfo(s.config, name)
}
// ResolveIndex takes indexName and returns index info
func (s *Service) ResolveIndex(name string) (*registrytypes.IndexInfo, error) {
func (s *DefaultService) ResolveIndex(name string) (*registrytypes.IndexInfo, error) {
return newIndexInfo(s.config, name)
}
@ -155,25 +171,25 @@ func (e APIEndpoint) ToV1Endpoint(userAgent string, metaHeaders http.Header) (*V
}
// TLSConfig constructs a client TLS configuration based on server defaults
func (s *Service) TLSConfig(hostname string) (*tls.Config, error) {
func (s *DefaultService) TLSConfig(hostname string) (*tls.Config, error) {
return newTLSConfig(hostname, isSecureIndex(s.config, hostname))
}
func (s *Service) tlsConfigForMirror(mirrorURL *url.URL) (*tls.Config, error) {
func (s *DefaultService) tlsConfigForMirror(mirrorURL *url.URL) (*tls.Config, error) {
return s.TLSConfig(mirrorURL.Host)
}
// LookupPullEndpoints creates a list of endpoints to try to pull from, in order of preference.
// It gives preference to v2 endpoints over v1, mirrors over the actual
// registry, and HTTPS over plain HTTP.
func (s *Service) LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
func (s *DefaultService) LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
return s.lookupEndpoints(hostname)
}
// LookupPushEndpoints creates a list of endpoints to try to push to, in order of preference.
// It gives preference to v2 endpoints over v1, and HTTPS over plain HTTP.
// Mirrors are not included.
func (s *Service) LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
func (s *DefaultService) LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
allEndpoints, err := s.lookupEndpoints(hostname)
if err == nil {
for _, endpoint := range allEndpoints {
@ -185,7 +201,7 @@ func (s *Service) LookupPushEndpoints(hostname string) (endpoints []APIEndpoint,
return endpoints, err
}
func (s *Service) lookupEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
func (s *DefaultService) lookupEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
endpoints, err = s.lookupV2Endpoints(hostname)
if err != nil {
return nil, err

View File

@ -6,7 +6,7 @@ import (
"github.com/docker/go-connections/tlsconfig"
)
func (s *Service) lookupV1Endpoints(hostname string) (endpoints []APIEndpoint, err error) {
func (s *DefaultService) lookupV1Endpoints(hostname string) (endpoints []APIEndpoint, err error) {
var cfg = tlsconfig.ServerDefault
tlsConfig := &cfg
if hostname == DefaultNamespace {

View File

@ -7,7 +7,7 @@ import (
"github.com/docker/go-connections/tlsconfig"
)
func (s *Service) lookupV2Endpoints(hostname string) (endpoints []APIEndpoint, err error) {
func (s *DefaultService) lookupV2Endpoints(hostname string) (endpoints []APIEndpoint, err error) {
var cfg = tlsconfig.ServerDefault
tlsConfig := &cfg
if hostname == DefaultNamespace || hostname == DefaultV1Registry.Host {