diff --git a/internal/registry/auth.go b/internal/registry/auth.go index f99c58a121..8799686531 100644 --- a/internal/registry/auth.go +++ b/internal/registry/auth.go @@ -127,44 +127,6 @@ func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifi }, nil } -// ConvertToHostname normalizes a registry URL which has http|https prepended -// to just its hostname. It is used to match credentials, which may be either -// stored as hostname or as hostname including scheme (in legacy configuration -// files). -func ConvertToHostname(maybeURL string) string { - stripped := maybeURL - if scheme, remainder, ok := strings.Cut(stripped, "://"); ok { - switch scheme { - case "http", "https": - stripped = remainder - default: - // unknown, or no scheme; doing nothing for now, as we never did. - } - } - stripped, _, _ = strings.Cut(stripped, "/") - return stripped -} - -// ResolveAuthConfig matches an auth configuration to a server address or a URL -func ResolveAuthConfig(authConfigs map[string]registry.AuthConfig, index *registry.IndexInfo) registry.AuthConfig { - configKey := GetAuthConfigKey(index) - // First try the happy case - if c, found := authConfigs[configKey]; found || index.Official { - return c - } - - // Maybe they have a legacy config file, we will iterate the keys converting - // them to the new format and testing - for registryURL, ac := range authConfigs { - if configKey == ConvertToHostname(registryURL) { - return ac - } - } - - // When all else fails, return an empty auth config - return registry.AuthConfig{} -} - // PingResponseError is used when the response from a ping // was received but invalid. type PingResponseError struct { diff --git a/internal/registry/auth_test.go b/internal/registry/auth_test.go deleted file mode 100644 index 927254177d..0000000000 --- a/internal/registry/auth_test.go +++ /dev/null @@ -1,106 +0,0 @@ -package registry - -import ( - "testing" - - "github.com/docker/docker/api/types/registry" - "gotest.tools/v3/assert" -) - -func buildAuthConfigs() map[string]registry.AuthConfig { - authConfigs := map[string]registry.AuthConfig{} - - for _, reg := range []string{"testIndex", IndexServer} { - authConfigs[reg] = registry.AuthConfig{ - Username: "docker-user", - Password: "docker-pass", - } - } - - return authConfigs -} - -func TestResolveAuthConfigIndexServer(t *testing.T) { - authConfigs := buildAuthConfigs() - indexConfig := authConfigs[IndexServer] - - officialIndex := ®istry.IndexInfo{ - Official: true, - } - privateIndex := ®istry.IndexInfo{ - Official: false, - } - - resolved := ResolveAuthConfig(authConfigs, officialIndex) - assert.Equal(t, resolved, indexConfig, "Expected ResolveAuthConfig to return IndexServer") - - resolved = ResolveAuthConfig(authConfigs, privateIndex) - assert.Check(t, resolved != indexConfig, "Expected ResolveAuthConfig to not return IndexServer") -} - -func TestResolveAuthConfigFullURL(t *testing.T) { - authConfigs := buildAuthConfigs() - - registryAuth := registry.AuthConfig{ - Username: "foo-user", - Password: "foo-pass", - } - localAuth := registry.AuthConfig{ - Username: "bar-user", - Password: "bar-pass", - } - officialAuth := registry.AuthConfig{ - Username: "baz-user", - Password: "baz-pass", - } - authConfigs[IndexServer] = officialAuth - - expectedAuths := map[string]registry.AuthConfig{ - "registry.example.com": registryAuth, - "localhost:8000": localAuth, - "example.com": localAuth, - } - - validRegistries := map[string][]string{ - "registry.example.com": { - "https://registry.example.com/v1/", - "http://registry.example.com/v1/", - "registry.example.com", - "registry.example.com/v1/", - }, - "localhost:8000": { - "https://localhost:8000/v1/", - "http://localhost:8000/v1/", - "localhost:8000", - "localhost:8000/v1/", - }, - "example.com": { - "https://example.com/v1/", - "http://example.com/v1/", - "example.com", - "example.com/v1/", - }, - } - - for configKey, registries := range validRegistries { - configured, ok := expectedAuths[configKey] - if !ok { - t.Fail() - } - index := ®istry.IndexInfo{ - Name: configKey, - } - for _, reg := range registries { - authConfigs[reg] = configured - resolved := ResolveAuthConfig(authConfigs, index) - if resolved.Username != configured.Username || resolved.Password != configured.Password { - t.Errorf("%s -> %v != %v\n", reg, resolved, configured) - } - delete(authConfigs, reg) - resolved = ResolveAuthConfig(authConfigs, index) - if resolved.Username == configured.Username || resolved.Password == configured.Password { - t.Errorf("%s -> %v == %v\n", reg, resolved, configured) - } - } - } -} diff --git a/internal/registry/config.go b/internal/registry/config.go index 1aa466147c..dac5e7fd6c 100644 --- a/internal/registry/config.go +++ b/internal/registry/config.go @@ -106,19 +106,6 @@ func newServiceConfig(options ServiceOptions) (*serviceConfig, error) { return config, nil } -// copy constructs a new ServiceConfig with a copy of the configuration in config. -func (config *serviceConfig) copy() *registry.ServiceConfig { - ic := make(map[string]*registry.IndexInfo) - for key, value := range config.IndexConfigs { - ic[key] = value - } - return ®istry.ServiceConfig{ - InsecureRegistryCIDRs: append([]*registry.NetIPNet(nil), config.InsecureRegistryCIDRs...), - IndexConfigs: ic, - Mirrors: append([]string(nil), config.Mirrors...), - } -} - // loadMirrors loads mirrors to config, after removing duplicates. // Returns an error if mirrors contains an invalid mirror. func (config *serviceConfig) loadMirrors(mirrors []string) error { @@ -320,18 +307,12 @@ func ValidateIndexName(val string) (string, error) { } func normalizeIndexName(val string) string { - // TODO(thaJeztah): consider normalizing other known options, such as "(https://)registry-1.docker.io", "https://index.docker.io/v1/". - // TODO: upstream this to check to reference package if val == "index.docker.io" { return "docker.io" } return val } -func hasScheme(reposName string) bool { - return strings.Contains(reposName, "://") -} - func validateHostPort(s string) error { // Split host and port, and in case s can not be split, assume host only host, port, err := net.SplitHostPort(s) @@ -356,32 +337,6 @@ func validateHostPort(s string) error { return nil } -// newIndexInfo returns IndexInfo configuration from indexName -func newIndexInfo(config *serviceConfig, indexName string) *registry.IndexInfo { - indexName = normalizeIndexName(indexName) - - // Return any configured index info, first. - if index, ok := config.IndexConfigs[indexName]; ok { - return index - } - - // Construct a non-configured index info. - return ®istry.IndexInfo{ - Name: indexName, - Mirrors: []string{}, - Secure: config.isSecureIndex(indexName), - } -} - -// GetAuthConfigKey special-cases using the full index address of the official -// index as the AuthConfig key, and uses the (host)name[:port] for private indexes. -func GetAuthConfigKey(index *registry.IndexInfo) string { - if index.Official { - return IndexServer - } - return index.Name -} - // ParseRepositoryInfo performs the breakdown of a repository name into a // [RepositoryInfo], but lacks registry configuration. // diff --git a/internal/registry/errors.go b/internal/registry/errors.go index d37155a789..4174c91d15 100644 --- a/internal/registry/errors.go +++ b/internal/registry/errors.go @@ -49,19 +49,3 @@ func (invalidParameterErr) InvalidParameter() {} func (e invalidParameterErr) Unwrap() error { return e.error } - -type systemErr struct{ error } - -func (systemErr) System() {} - -func (e systemErr) Unwrap() error { - return e.error -} - -type errUnknown struct{ error } - -func (errUnknown) Unknown() {} - -func (e errUnknown) Unwrap() error { - return e.error -} diff --git a/internal/registry/registry_mock_test.go b/internal/registry/registry_mock_test.go index eb6b16de79..6ccabce5bc 100644 --- a/internal/registry/registry_mock_test.go +++ b/internal/registry/registry_mock_test.go @@ -13,10 +13,7 @@ import ( "gotest.tools/v3/assert" ) -var ( - testHTTPServer *httptest.Server - testHTTPSServer *httptest.Server -) +var testHTTPServer *httptest.Server func init() { r := http.NewServeMux() @@ -29,7 +26,6 @@ func init() { r.HandleFunc("/v2/version", handlerGetPing) testHTTPServer = httptest.NewServer(handlerAccessLog(r)) - testHTTPSServer = httptest.NewTLSServer(handlerAccessLog(r)) } func handlerAccessLog(handler http.Handler) http.Handler { @@ -44,30 +40,6 @@ func makeURL(req string) string { return testHTTPServer.URL + req } -func makeHTTPSURL(req string) string { - return testHTTPSServer.URL + req -} - -func makeIndex(req string) *registry.IndexInfo { - return ®istry.IndexInfo{ - Name: makeURL(req), - } -} - -func makeHTTPSIndex(req string) *registry.IndexInfo { - return ®istry.IndexInfo{ - Name: makeHTTPSURL(req), - } -} - -func makePublicIndex() *registry.IndexInfo { - return ®istry.IndexInfo{ - Name: IndexServer, - Secure: true, - Official: true, - } -} - func writeHeaders(w http.ResponseWriter) { h := w.Header() h.Add("Server", "docker-tests/mock") diff --git a/internal/registry/registry_test.go b/internal/registry/registry_test.go index 8495174c36..94d05eb472 100644 --- a/internal/registry/registry_test.go +++ b/internal/registry/registry_test.go @@ -34,6 +34,23 @@ func overrideLookupIP(t *testing.T) { }) } +// newIndexInfo returns IndexInfo configuration from indexName +func newIndexInfo(config *serviceConfig, indexName string) *registry.IndexInfo { + indexName = normalizeIndexName(indexName) + + // Return any configured index info, first. + if index, ok := config.IndexConfigs[indexName]; ok { + return index + } + + // Construct a non-configured index info. + return ®istry.IndexInfo{ + Name: indexName, + Mirrors: []string{}, + Secure: config.isSecureIndex(indexName), + } +} + func TestParseRepositoryInfo(t *testing.T) { type staticRepositoryInfo struct { Index *registry.IndexInfo diff --git a/internal/registry/search.go b/internal/registry/search.go deleted file mode 100644 index 26a14298ac..0000000000 --- a/internal/registry/search.go +++ /dev/null @@ -1,170 +0,0 @@ -package registry - -import ( - "context" - "net/http" - "strconv" - "strings" - - "github.com/containerd/log" - "github.com/docker/distribution/registry/client/auth" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/registry" - "github.com/pkg/errors" -) - -var acceptedSearchFilterTags = map[string]bool{ - "is-automated": true, // Deprecated: the "is_automated" field is deprecated and will always be false in the future. - "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 - } - - // "is-automated" is deprecated and filtering for `true` will yield no results. - if isAutomated { - return []registry.SearchResult{}, nil - } - - 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, invalidParameterErr{errors.Wrapf(err, "invalid filter 'stars=%s'", hasStar)} - } - if iHasStar > hasStarFilter { - hasStarFilter = iHasStar - } - } - } - - unfilteredResult, err := s.searchUnfiltered(ctx, term, limit, authConfig, headers) - if err != nil { - return nil, err - } - - filteredResults := []registry.SearchResult{} - for _, result := range unfilteredResult.Results { - if searchFilters.Contains("is-official") { - if isOfficial != result.IsOfficial { - continue - } - } - if searchFilters.Contains("stars") { - if result.StarCount < hasStarFilter { - continue - } - } - // "is-automated" is deprecated and the value in Docker Hub search - // results is untrustworthy. Force it to false so as to not mislead our - // clients. - result.IsAutomated = false //nolint:staticcheck // ignore SA1019 (field is deprecated) - filteredResults = append(filteredResults, result) - } - - return filteredResults, nil -} - -func (s *Service) searchUnfiltered(ctx context.Context, term string, limit int, authConfig *registry.AuthConfig, headers http.Header) (*registry.SearchResults, error) { - 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 := newIndexInfo(s.config, indexName) - s.mu.RUnlock() - if index.Official { - // If pull "library/foo", it's stored locally under "foo" - remoteName = strings.TrimPrefix(remoteName, "library/") - } - - endpoint, err := newV1Endpoint(ctx, index, headers) - if err != nil { - return nil, err - } - - var client *http.Client - if authConfig != nil && authConfig.IdentityToken != "" && authConfig.Username != "" { - creds := NewStaticCredentialStore(authConfig) - - // TODO(thaJeztah); is there a reason not to include other headers here? (originally added in 19d48f0b8ba59eea9f2cac4ad1c7977712a6b7ac) - modifiers := Headers(headers.Get("User-Agent"), nil) - v2Client, err := v2AuthHTTPClient(endpoint.URL, endpoint.client.Transport, modifiers, creds, []auth.Scope{ - auth.RegistryScope{Name: "catalog", Actions: []string{"search"}}, - }) - 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 - - log.G(ctx).Debugf("using v2 client for search to %s", endpoint.URL) - client = v2Client - } else { - client = endpoint.client - if err := authorizeClient(ctx, client, authConfig, endpoint); err != nil { - return nil, err - } - } - - return newSession(client, endpoint).searchRepositories(ctx, remoteName, limit) -} - -// 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) - indexName = normalizeIndexName(indexName) - if indexName == IndexName { - return ®istry.IndexInfo{ - Name: IndexName, - Mirrors: []string{}, - Secure: true, - Official: true, - }, nil - } - - return ®istry.IndexInfo{ - Name: indexName, - Mirrors: []string{}, - Secure: !isInsecure(indexName), - }, nil -} diff --git a/internal/registry/search_endpoint_v1.go b/internal/registry/search_endpoint_v1.go deleted file mode 100644 index 2b20c6044e..0000000000 --- a/internal/registry/search_endpoint_v1.go +++ /dev/null @@ -1,213 +0,0 @@ -package registry - -import ( - "context" - "crypto/tls" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/url" - "strings" - - "github.com/containerd/log" - "github.com/docker/distribution/registry/client/transport" - "github.com/docker/docker/api/types/registry" -) - -// v1PingResult contains the information returned when pinging a registry. It -// indicates whether the registry claims to be a standalone registry. -type v1PingResult struct { - // Standalone is set to true if the registry indicates it is a - // standalone registry in the X-Docker-Registry-Standalone - // header - Standalone bool `json:"standalone"` -} - -// v1Endpoint stores basic information about a V1 registry endpoint. -type v1Endpoint struct { - client *http.Client - URL *url.URL - IsSecure bool -} - -// newV1Endpoint parses the given address to return a registry endpoint. -// TODO: remove. This is only used by search. -func newV1Endpoint(ctx context.Context, index *registry.IndexInfo, headers http.Header) (*v1Endpoint, error) { - tlsConfig, err := newTLSConfig(ctx, index.Name, index.Secure) - if err != nil { - return nil, err - } - - endpoint, err := newV1EndpointFromStr(GetAuthConfigKey(index), tlsConfig, headers) - if err != nil { - return nil, err - } - - if endpoint.String() == IndexServer { - // Skip the check, we know this one is valid - // (and we never want to fall back to http in case of error) - return endpoint, nil - } - - // Try HTTPS ping to registry - endpoint.URL.Scheme = "https" - if _, err := endpoint.ping(ctx); err != nil { - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - return nil, err - } - if endpoint.IsSecure { - // If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry` - // in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fall back to HTTP. - hint := fmt.Sprintf( - ". If this private registry supports only HTTP or HTTPS with an unknown CA certificate, add `--insecure-registry %[1]s` to the daemon's arguments. "+ - "In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; place the CA certificate at /etc/docker/certs.d/%[1]s/ca.crt", - endpoint.URL.Host, - ) - return nil, invalidParamf("invalid registry endpoint %s: %v%s", endpoint, err, hint) - } - - // registry is insecure and HTTPS failed, fallback to HTTP. - log.G(ctx).WithError(err).Debugf("error from registry %q marked as insecure - insecurely falling back to HTTP", endpoint) - endpoint.URL.Scheme = "http" - if _, err2 := endpoint.ping(ctx); err2 != nil { - return nil, invalidParamf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2) - } - } - - return endpoint, nil -} - -// trimV1Address trims the "v1" version suffix off the address and returns -// the trimmed address. It returns an error on "v2" endpoints. -func trimV1Address(address string) (string, error) { - trimmed := strings.TrimSuffix(address, "/") - if strings.HasSuffix(trimmed, "/v2") { - return "", invalidParamf("search is not supported on v2 endpoints: %s", address) - } - return strings.TrimSuffix(trimmed, "/v1"), nil -} - -func newV1EndpointFromStr(address string, tlsConfig *tls.Config, headers http.Header) (*v1Endpoint, error) { - if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") { - address = "https://" + address - } - - address, err := trimV1Address(address) - if err != nil { - return nil, err - } - - uri, err := url.Parse(address) - if err != nil { - return nil, invalidParam(err) - } - - // TODO(tiborvass): make sure a ConnectTimeout transport is used - tr := newTransport(tlsConfig) - - return &v1Endpoint{ - IsSecure: tlsConfig == nil || !tlsConfig.InsecureSkipVerify, - URL: uri, - client: httpClient(transport.NewTransport(tr, Headers("", headers)...)), - }, nil -} - -// Get the formatted URL for the root of this registry Endpoint -func (e *v1Endpoint) String() string { - return e.URL.String() + "/v1/" -} - -// ping returns a v1PingResult which indicates whether the registry is standalone or not. -func (e *v1Endpoint) ping(ctx context.Context) (v1PingResult, error) { - if e.String() == IndexServer { - // Skip the check, we know this one is valid - // (and we never want to fallback to http in case of error) - return v1PingResult{}, nil - } - - pingURL := e.String() + "_ping" - log.G(ctx).WithField("url", pingURL).Debug("attempting v1 ping for registry endpoint") - req, err := http.NewRequestWithContext(ctx, http.MethodGet, pingURL, http.NoBody) - if err != nil { - return v1PingResult{}, invalidParam(err) - } - - resp, err := e.client.Do(req) - if err != nil { - if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - return v1PingResult{}, err - } - return v1PingResult{}, invalidParam(err) - } - - defer resp.Body.Close() - - if v := resp.Header.Get("X-Docker-Registry-Standalone"); v != "" { - info := v1PingResult{} - // Accepted values are "1", and "true" (case-insensitive). - if v == "1" || strings.EqualFold(v, "true") { - info.Standalone = true - } - log.G(ctx).Debugf("v1PingResult.Standalone (from X-Docker-Registry-Standalone header): %t", info.Standalone) - return info, nil - } - - // If the header is absent, we assume true for compatibility with earlier - // versions of the registry. default to true - info := v1PingResult{ - Standalone: true, - } - if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { - log.G(ctx).WithError(err).Debug("error unmarshaling _ping response") - // don't stop here. Just assume sane defaults - } - - log.G(ctx).Debugf("v1PingResult.Standalone: %t", info.Standalone) - return info, nil -} - -// httpClient returns an HTTP client structure which uses the given transport -// and contains the necessary headers for redirected requests -func httpClient(tr http.RoundTripper) *http.Client { - return &http.Client{ - Transport: tr, - CheckRedirect: addRequiredHeadersToRedirectedRequests, - } -} - -func trustedLocation(req *http.Request) bool { - var ( - trusteds = []string{"docker.com", "docker.io"} - hostname = strings.SplitN(req.Host, ":", 2)[0] - ) - if req.URL.Scheme != "https" { - return false - } - - for _, trusted := range trusteds { - if hostname == trusted || strings.HasSuffix(hostname, "."+trusted) { - return true - } - } - return false -} - -// addRequiredHeadersToRedirectedRequests adds the necessary redirection headers -// for redirected requests -func addRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Request) error { - if len(via) != 0 && via[0] != nil { - if trustedLocation(req) && trustedLocation(via[0]) { - req.Header = via[0].Header - return nil - } - for k, v := range via[0].Header { - if k != "Authorization" { - for _, vv := range v { - req.Header.Add(k, vv) - } - } - } - } - return nil -} diff --git a/internal/registry/search_endpoint_v1_test.go b/internal/registry/search_endpoint_v1_test.go deleted file mode 100644 index a430f34cc1..0000000000 --- a/internal/registry/search_endpoint_v1_test.go +++ /dev/null @@ -1,237 +0,0 @@ -package registry - -import ( - "context" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/docker/docker/api/types/registry" - "gotest.tools/v3/assert" - is "gotest.tools/v3/assert/cmp" -) - -func TestV1EndpointPing(t *testing.T) { - testPing := func(index *registry.IndexInfo, expectedStandalone bool, assertMessage string) { - ep, err := newV1Endpoint(context.Background(), index, nil) - if err != nil { - t.Fatal(err) - } - regInfo, err := ep.ping(context.Background()) - if err != nil { - t.Fatal(err) - } - - assert.Equal(t, regInfo.Standalone, expectedStandalone, assertMessage) - } - - testPing(makeIndex("/v1/"), true, "Expected standalone to be true (default)") - testPing(makeHTTPSIndex("/v1/"), true, "Expected standalone to be true (default)") - testPing(makePublicIndex(), false, "Expected standalone to be false for public index") -} - -func TestV1Endpoint(t *testing.T) { - // Simple wrapper to fail test if err != nil - expandEndpoint := func(index *registry.IndexInfo) *v1Endpoint { - endpoint, err := newV1Endpoint(context.Background(), index, nil) - if err != nil { - t.Fatal(err) - } - return endpoint - } - - assertInsecureIndex := func(index *registry.IndexInfo) { - index.Secure = true - _, err := newV1Endpoint(context.Background(), index, nil) - assert.ErrorContains(t, err, "insecure-registry", index.Name+": Expected insecure-registry error for insecure index") - index.Secure = false - } - - assertSecureIndex := func(index *registry.IndexInfo) { - index.Secure = true - _, err := newV1Endpoint(context.Background(), index, nil) - assert.ErrorContains(t, err, "certificate signed by unknown authority", index.Name+": Expected cert error for secure index") - index.Secure = false - } - - index := ®istry.IndexInfo{} - index.Name = makeURL("/v1/") - endpoint := expandEndpoint(index) - assert.Equal(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name) - assertInsecureIndex(index) - - index.Name = makeURL("") - endpoint = expandEndpoint(index) - assert.Equal(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/") - assertInsecureIndex(index) - - httpURL := makeURL("") - index.Name = strings.SplitN(httpURL, "://", 2)[1] - endpoint = expandEndpoint(index) - assert.Equal(t, endpoint.String(), httpURL+"/v1/", index.Name+": Expected endpoint to be "+httpURL+"/v1/") - assertInsecureIndex(index) - - index.Name = makeHTTPSURL("/v1/") - endpoint = expandEndpoint(index) - assert.Equal(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name) - assertSecureIndex(index) - - index.Name = makeHTTPSURL("") - endpoint = expandEndpoint(index) - assert.Equal(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/") - assertSecureIndex(index) - - httpsURL := makeHTTPSURL("") - index.Name = strings.SplitN(httpsURL, "://", 2)[1] - endpoint = expandEndpoint(index) - assert.Equal(t, endpoint.String(), httpsURL+"/v1/", index.Name+": Expected endpoint to be "+httpsURL+"/v1/") - assertSecureIndex(index) - - badEndpoints := []string{ - "http://127.0.0.1/v1/", - "https://127.0.0.1/v1/", - "http://127.0.0.1", - "https://127.0.0.1", - "127.0.0.1", - } - for _, address := range badEndpoints { - index.Name = address - _, err := newV1Endpoint(context.Background(), index, nil) - assert.Check(t, err != nil, "Expected error while expanding bad endpoint: %s", address) - } -} - -func TestV1EndpointParse(t *testing.T) { - tests := []struct { - address string - expected string - expectedErr string - }{ - { - address: IndexServer, - expected: IndexServer, - }, - { - address: "https://0.0.0.0:5000/v1/", - expected: "https://0.0.0.0:5000/v1/", - }, - { - address: "https://0.0.0.0:5000", - expected: "https://0.0.0.0:5000/v1/", - }, - { - address: "0.0.0.0:5000", - expected: "https://0.0.0.0:5000/v1/", - }, - { - address: "https://0.0.0.0:5000/nonversion/", - expected: "https://0.0.0.0:5000/nonversion/v1/", - }, - { - address: "https://0.0.0.0:5000/v0/", - expected: "https://0.0.0.0:5000/v0/v1/", - }, - { - address: "https://0.0.0.0:5000/v2/", - expectedErr: "search is not supported on v2 endpoints: https://0.0.0.0:5000/v2/", - }, - } - for _, tc := range tests { - t.Run(tc.address, func(t *testing.T) { - ep, err := newV1EndpointFromStr(tc.address, nil, nil) - if tc.expectedErr != "" { - assert.Check(t, is.Error(err, tc.expectedErr)) - assert.Check(t, is.Nil(ep)) - } else { - assert.NilError(t, err) - assert.Check(t, is.Equal(ep.String(), tc.expected)) - } - }) - } -} - -// Ensure that a registry endpoint that responds with a 401 only is determined -// to be a valid v1 registry endpoint -func TestV1EndpointValidate(t *testing.T) { - requireBasicAuthHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("WWW-Authenticate", `Basic realm="localhost"`) - w.WriteHeader(http.StatusUnauthorized) - }) - - // Make a test server which should validate as a v1 server. - testServer := httptest.NewServer(requireBasicAuthHandler) - defer testServer.Close() - - testEndpoint, err := newV1Endpoint(context.Background(), ®istry.IndexInfo{Name: testServer.URL}, nil) - if err != nil { - t.Fatal(err) - } - - if testEndpoint.URL.Scheme != "http" { - t.Fatalf("expecting to validate endpoint as http, got url %s", testEndpoint.String()) - } -} - -func TestTrustedLocation(t *testing.T) { - for _, u := range []string{"http://example.com", "https://example.com:7777", "http://docker.io", "http://test.docker.com", "https://fakedocker.com"} { - req, _ := http.NewRequest(http.MethodGet, u, http.NoBody) - assert.Check(t, !trustedLocation(req)) - } - - for _, u := range []string{"https://docker.io", "https://test.docker.com:80"} { - req, _ := http.NewRequest(http.MethodGet, u, http.NoBody) - assert.Check(t, trustedLocation(req)) - } -} - -func TestAddRequiredHeadersToRedirectedRequests(t *testing.T) { - for _, urls := range [][]string{ - {"http://docker.io", "https://docker.com"}, - {"https://foo.docker.io:7777", "http://bar.docker.com"}, - {"https://foo.docker.io", "https://example.com"}, - } { - reqFrom, _ := http.NewRequest(http.MethodGet, urls[0], http.NoBody) - reqFrom.Header.Add("Content-Type", "application/json") - reqFrom.Header.Add("Authorization", "super_secret") - reqTo, _ := http.NewRequest(http.MethodGet, urls[1], http.NoBody) - - _ = addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom}) - - if len(reqTo.Header) != 1 { - t.Fatalf("Expected 1 headers, got %d", len(reqTo.Header)) - } - - if reqTo.Header.Get("Content-Type") != "application/json" { - t.Fatal("'Content-Type' should be 'application/json'") - } - - if reqTo.Header.Get("Authorization") != "" { - t.Fatal("'Authorization' should be empty") - } - } - - for _, urls := range [][]string{ - {"https://docker.io", "https://docker.com"}, - {"https://foo.docker.io:7777", "https://bar.docker.com"}, - } { - reqFrom, _ := http.NewRequest(http.MethodGet, urls[0], http.NoBody) - reqFrom.Header.Add("Content-Type", "application/json") - reqFrom.Header.Add("Authorization", "super_secret") - reqTo, _ := http.NewRequest(http.MethodGet, urls[1], http.NoBody) - - _ = addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom}) - - if len(reqTo.Header) != 2 { - t.Fatalf("Expected 2 headers, got %d", len(reqTo.Header)) - } - - if reqTo.Header.Get("Content-Type") != "application/json" { - t.Fatal("'Content-Type' should be 'application/json'") - } - - if reqTo.Header.Get("Authorization") != "super_secret" { - t.Fatal("'Authorization' should be 'super_secret'") - } - } -} diff --git a/internal/registry/search_session.go b/internal/registry/search_session.go deleted file mode 100644 index 825777ac2c..0000000000 --- a/internal/registry/search_session.go +++ /dev/null @@ -1,248 +0,0 @@ -package registry - -import ( - // this is required for some certificates - "context" - _ "crypto/sha512" - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/cookiejar" - "net/url" - "strconv" - "strings" - "sync" - - "github.com/containerd/log" - "github.com/docker/docker/api/types/registry" - "github.com/pkg/errors" -) - -// A session is used to communicate with a V1 registry -type session struct { - indexEndpoint *v1Endpoint - client *http.Client -} - -type authTransport struct { - base http.RoundTripper - authConfig *registry.AuthConfig - - alwaysSetBasicAuth bool - token []string - - mu sync.Mutex // guards modReq - modReq map[*http.Request]*http.Request // original -> modified -} - -// newAuthTransport handles the auth layer when communicating with a v1 registry (private or official) -// -// For private v1 registries, set alwaysSetBasicAuth to true. -// -// For the official v1 registry, if there isn't already an Authorization header in the request, -// but there is an X-Docker-Token header set to true, then Basic Auth will be used to set the Authorization header. -// After sending the request with the provided base http.RoundTripper, if an X-Docker-Token header, representing -// a token, is present in the response, then it gets cached and sent in the Authorization header of all subsequent -// requests. -// -// If the server sends a token without the client having requested it, it is ignored. -// -// This RoundTripper also has a CancelRequest method important for correct timeout handling. -func newAuthTransport(base http.RoundTripper, authConfig *registry.AuthConfig, alwaysSetBasicAuth bool) *authTransport { - if base == nil { - base = http.DefaultTransport - } - return &authTransport{ - base: base, - authConfig: authConfig, - alwaysSetBasicAuth: alwaysSetBasicAuth, - modReq: make(map[*http.Request]*http.Request), - } -} - -// cloneRequest returns a clone of the provided *http.Request. -// The clone is a shallow copy of the struct and its Header map. -func cloneRequest(r *http.Request) *http.Request { - // shallow copy of the struct - r2 := new(http.Request) - *r2 = *r - // deep copy of the Header - r2.Header = make(http.Header, len(r.Header)) - for k, s := range r.Header { - r2.Header[k] = append([]string(nil), s...) - } - - return r2 -} - -// onEOFReader wraps an io.ReadCloser and a function -// the function will run at the end of file or close the file. -type onEOFReader struct { - Rc io.ReadCloser - Fn func() -} - -func (r *onEOFReader) Read(p []byte) (int, error) { - n, err := r.Rc.Read(p) - if err == io.EOF { - r.runFunc() - } - return n, err -} - -// Close closes the file and run the function. -func (r *onEOFReader) Close() error { - err := r.Rc.Close() - r.runFunc() - return err -} - -func (r *onEOFReader) runFunc() { - if fn := r.Fn; fn != nil { - fn() - r.Fn = nil - } -} - -// RoundTrip changes an HTTP request's headers to add the necessary -// authentication-related headers -func (tr *authTransport) RoundTrip(orig *http.Request) (*http.Response, error) { - // Authorization should not be set on 302 redirect for untrusted locations. - // This logic mirrors the behavior in addRequiredHeadersToRedirectedRequests. - // As the authorization logic is currently implemented in RoundTrip, - // a 302 redirect is detected by looking at the Referrer header as go http package adds said header. - // This is safe as Docker doesn't set Referrer in other scenarios. - if orig.Header.Get("Referer") != "" && !trustedLocation(orig) { - return tr.base.RoundTrip(orig) - } - - req := cloneRequest(orig) - tr.mu.Lock() - tr.modReq[orig] = req - tr.mu.Unlock() - - if tr.alwaysSetBasicAuth { - if tr.authConfig == nil { - return nil, errors.New("unexpected error: empty auth config") - } - req.SetBasicAuth(tr.authConfig.Username, tr.authConfig.Password) - return tr.base.RoundTrip(req) - } - - // Don't override - if req.Header.Get("Authorization") == "" { - if req.Header.Get("X-Docker-Token") == "true" && tr.authConfig != nil && tr.authConfig.Username != "" { - req.SetBasicAuth(tr.authConfig.Username, tr.authConfig.Password) - } else if len(tr.token) > 0 { - req.Header.Set("Authorization", "Token "+strings.Join(tr.token, ",")) - } - } - resp, err := tr.base.RoundTrip(req) - if err != nil { - tr.mu.Lock() - delete(tr.modReq, orig) - tr.mu.Unlock() - return nil, err - } - if len(resp.Header["X-Docker-Token"]) > 0 { - tr.token = resp.Header["X-Docker-Token"] - } - resp.Body = &onEOFReader{ - Rc: resp.Body, - Fn: func() { - tr.mu.Lock() - delete(tr.modReq, orig) - tr.mu.Unlock() - }, - } - return resp, nil -} - -// CancelRequest cancels an in-flight request by closing its connection. -func (tr *authTransport) CancelRequest(req *http.Request) { - type canceler interface { - CancelRequest(*http.Request) - } - if cr, ok := tr.base.(canceler); ok { - tr.mu.Lock() - modReq := tr.modReq[req] - delete(tr.modReq, req) - tr.mu.Unlock() - cr.CancelRequest(modReq) - } -} - -func authorizeClient(ctx context.Context, client *http.Client, authConfig *registry.AuthConfig, endpoint *v1Endpoint) error { - var alwaysSetBasicAuth bool - - // If we're working with a standalone private registry over HTTPS, send Basic Auth headers - // alongside all our requests. - if endpoint.String() != IndexServer && endpoint.URL.Scheme == "https" { - info, err := endpoint.ping(ctx) - if err != nil { - return err - } - if info.Standalone && authConfig != nil { - log.G(ctx).WithField("endpoint", endpoint.String()).Debug("Endpoint is eligible for private registry; enabling alwaysSetBasicAuth") - alwaysSetBasicAuth = true - } - } - - // Annotate the transport unconditionally so that v2 can - // properly fallback on v1 when an image is not found. - client.Transport = newAuthTransport(client.Transport, authConfig, alwaysSetBasicAuth) - - jar, err := cookiejar.New(nil) - if err != nil { - return systemErr{errors.New("cookiejar.New is not supposed to return an error")} - } - client.Jar = jar - - return nil -} - -func newSession(client *http.Client, endpoint *v1Endpoint) *session { - return &session{ - client: client, - indexEndpoint: endpoint, - } -} - -// defaultSearchLimit is the default value for maximum number of returned search results. -const defaultSearchLimit = 25 - -// searchRepositories performs a search against the remote repository -func (r *session) searchRepositories(ctx context.Context, term string, limit int) (*registry.SearchResults, error) { - if limit == 0 { - limit = defaultSearchLimit - } - if limit < 1 || limit > 100 { - return nil, invalidParamf("limit %d is outside the range of [1, 100]", limit) - } - u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term) + "&n=" + url.QueryEscape(strconv.Itoa(limit)) - log.G(ctx).WithField("url", u).Debug("searchRepositories") - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, http.NoBody) - if err != nil { - return nil, invalidParamWrapf(err, "error building request") - } - // Have the AuthTransport send authentication, when logged in. - req.Header.Set("X-Docker-Token", "true") - res, err := r.client.Do(req) - if err != nil { - return nil, systemErr{err} - } - defer res.Body.Close() - if res.StatusCode != http.StatusOK { - // TODO(thaJeztah): return upstream response body for errors (see https://github.com/moby/moby/issues/27286). - // TODO(thaJeztah): handle other status-codes to return correct error-type - return nil, errUnknown{fmt.Errorf("unexpected status code %d", res.StatusCode)} - } - result := ®istry.SearchResults{} - err = json.NewDecoder(res.Body).Decode(result) - if err != nil { - return nil, systemErr{errors.Wrap(err, "error decoding registry search results")} - } - return result, nil -} diff --git a/internal/registry/search_test.go b/internal/registry/search_test.go deleted file mode 100644 index d9f563df17..0000000000 --- a/internal/registry/search_test.go +++ /dev/null @@ -1,418 +0,0 @@ -package registry - -import ( - "context" - "encoding/json" - "net/http" - "net/http/httptest" - "net/http/httputil" - "testing" - - cerrdefs "github.com/containerd/errdefs" - "github.com/docker/distribution/registry/client/transport" - "github.com/docker/docker/api/types/filters" - "github.com/docker/docker/api/types/registry" - "gotest.tools/v3/assert" -) - -func spawnTestRegistrySession(t *testing.T) *session { - t.Helper() - authConfig := ®istry.AuthConfig{} - endpoint, err := newV1Endpoint(context.Background(), makeIndex("/v1/"), nil) - if err != nil { - t.Fatal(err) - } - userAgent := "docker test client" - var tr http.RoundTripper = debugTransport{newTransport(nil), t.Log} - tr = transport.NewTransport(newAuthTransport(tr, authConfig, false), Headers(userAgent, nil)...) - client := httpClient(tr) - - if err := authorizeClient(context.Background(), client, authConfig, endpoint); err != nil { - t.Fatal(err) - } - r := newSession(client, endpoint) - - // In a normal scenario for the v1 registry, the client should send a `X-Docker-Token: true` - // header while authenticating, in order to retrieve a token that can be later used to - // perform authenticated actions. - // - // The mock v1 registry does not support that, (TODO(tiborvass): support it), instead, - // it will consider authenticated any request with the header `X-Docker-Token: fake-token`. - // - // Because we know that the client's transport is an `*authTransport` we simply cast it, - // in order to set the internal cached token to the fake token, and thus send that fake token - // upon every subsequent requests. - r.client.Transport.(*authTransport).token = []string{"fake-token"} - return r -} - -type debugTransport struct { - http.RoundTripper - log func(...interface{}) -} - -func (tr debugTransport) RoundTrip(req *http.Request) (*http.Response, error) { - dump, err := httputil.DumpRequestOut(req, false) - if err != nil { - tr.log("could not dump request") - } - tr.log(string(dump)) - resp, err := tr.RoundTripper.RoundTrip(req) - if err != nil { - return nil, err - } - dump, err = httputil.DumpResponse(resp, false) - if err != nil { - tr.log("could not dump response") - } - tr.log(string(dump)) - return resp, err -} - -func TestSearchRepositories(t *testing.T) { - r := spawnTestRegistrySession(t) - results, err := r.searchRepositories(context.Background(), "fakequery", 25) - if err != nil { - t.Fatal(err) - } - if results == nil { - t.Fatal("Expected non-nil SearchResults object") - } - assert.Equal(t, results.NumResults, 1, "Expected 1 search results") - assert.Equal(t, results.Query, "fakequery", "Expected 'fakequery' as query") - assert.Equal(t, results.Results[0].StarCount, 42, "Expected 'fakeimage' to have 42 stars") -} - -func TestSearchErrors(t *testing.T) { - errorCases := []struct { - filtersArgs filters.Args - shouldReturnError bool - expectedError string - }{ - { - expectedError: "unexpected status code 500", - shouldReturnError: true, - }, - { - filtersArgs: filters.NewArgs(filters.Arg("type", "custom")), - expectedError: "invalid filter 'type'", - }, - { - filtersArgs: filters.NewArgs(filters.Arg("is-automated", "invalid")), - expectedError: "invalid filter 'is-automated=[invalid]'", - }, - { - filtersArgs: filters.NewArgs( - filters.Arg("is-automated", "true"), - filters.Arg("is-automated", "false"), - ), - expectedError: "invalid filter 'is-automated", - }, - { - filtersArgs: filters.NewArgs(filters.Arg("is-official", "invalid")), - expectedError: "invalid filter 'is-official=[invalid]'", - }, - { - filtersArgs: filters.NewArgs( - filters.Arg("is-official", "true"), - filters.Arg("is-official", "false"), - ), - expectedError: "invalid filter 'is-official", - }, - { - filtersArgs: filters.NewArgs(filters.Arg("stars", "invalid")), - expectedError: "invalid filter 'stars=invalid'", - }, - { - filtersArgs: filters.NewArgs( - filters.Arg("stars", "1"), - filters.Arg("stars", "invalid"), - ), - expectedError: "invalid filter 'stars=invalid'", - }, - } - for _, tc := range errorCases { - t.Run(tc.expectedError, func(t *testing.T) { - 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, cerrdefs.IsUnknown(err), "got: %T: %v", err, err) - return - } - assert.Check(t, cerrdefs.IsInvalidArgument(err), "got: %T: %v", err, err) - }) - } -} - -func TestSearch(t *testing.T) { - const term = "term" - successCases := []struct { - name string - filtersArgs filters.Args - registryResults []registry.SearchResult - expectedResults []registry.SearchResult - }{ - { - name: "empty results", - registryResults: []registry.SearchResult{}, - expectedResults: []registry.SearchResult{}, - }, - { - name: "no filter", - registryResults: []registry.SearchResult{ - { - Name: "name", - Description: "description", - }, - }, - expectedResults: []registry.SearchResult{ - { - Name: "name", - Description: "description", - }, - }, - }, - { - name: "is-automated=true, no results", - filtersArgs: filters.NewArgs(filters.Arg("is-automated", "true")), - registryResults: []registry.SearchResult{ - { - Name: "name", - Description: "description", - }, - }, - expectedResults: []registry.SearchResult{}, - }, - { - name: "is-automated=true", - filtersArgs: filters.NewArgs(filters.Arg("is-automated", "true")), - registryResults: []registry.SearchResult{ - { - Name: "name", - Description: "description", - IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated). - }, - }, - expectedResults: []registry.SearchResult{}, - }, - { - name: "is-automated=false, IsAutomated reset to false", - filtersArgs: filters.NewArgs(filters.Arg("is-automated", "false")), - registryResults: []registry.SearchResult{ - { - Name: "name", - Description: "description", - IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated). - }, - }, - expectedResults: []registry.SearchResult{ - { - Name: "name", - Description: "description", - IsAutomated: false, //nolint:staticcheck // ignore SA1019 (field is deprecated). - }, - }, - }, - { - name: "is-automated=false", - filtersArgs: filters.NewArgs(filters.Arg("is-automated", "false")), - registryResults: []registry.SearchResult{ - { - Name: "name", - Description: "description", - }, - }, - expectedResults: []registry.SearchResult{ - { - Name: "name", - Description: "description", - }, - }, - }, - { - name: "is-official=true, no results", - filtersArgs: filters.NewArgs(filters.Arg("is-official", "true")), - registryResults: []registry.SearchResult{ - { - Name: "name", - Description: "description", - }, - }, - expectedResults: []registry.SearchResult{}, - }, - { - name: "is-official=true", - filtersArgs: filters.NewArgs(filters.Arg("is-official", "true")), - registryResults: []registry.SearchResult{ - { - Name: "name", - Description: "description", - IsOfficial: true, - }, - }, - expectedResults: []registry.SearchResult{ - { - Name: "name", - Description: "description", - IsOfficial: true, - }, - }, - }, - { - name: "is-official=false, no results", - filtersArgs: filters.NewArgs(filters.Arg("is-official", "false")), - registryResults: []registry.SearchResult{ - { - Name: "name", - Description: "description", - IsOfficial: true, - }, - }, - expectedResults: []registry.SearchResult{}, - }, - { - name: "is-official=false", - filtersArgs: filters.NewArgs(filters.Arg("is-official", "false")), - registryResults: []registry.SearchResult{ - { - Name: "name", - Description: "description", - IsOfficial: false, - }, - }, - expectedResults: []registry.SearchResult{ - { - Name: "name", - Description: "description", - IsOfficial: false, - }, - }, - }, - { - name: "stars=0", - filtersArgs: filters.NewArgs(filters.Arg("stars", "0")), - registryResults: []registry.SearchResult{ - { - Name: "name", - Description: "description", - StarCount: 0, - }, - }, - expectedResults: []registry.SearchResult{ - { - Name: "name", - Description: "description", - StarCount: 0, - }, - }, - }, - { - name: "stars=0, no results", - filtersArgs: filters.NewArgs(filters.Arg("stars", "1")), - registryResults: []registry.SearchResult{ - { - Name: "name", - Description: "description", - StarCount: 0, - }, - }, - expectedResults: []registry.SearchResult{}, - }, - { - name: "stars=1", - filtersArgs: filters.NewArgs(filters.Arg("stars", "1")), - registryResults: []registry.SearchResult{ - { - Name: "name0", - Description: "description0", - StarCount: 0, - }, - { - Name: "name1", - Description: "description1", - StarCount: 1, - }, - }, - expectedResults: []registry.SearchResult{ - { - Name: "name1", - Description: "description1", - StarCount: 1, - }, - }, - }, - { - name: "stars=1, is-official=true, is-automated=true", - filtersArgs: filters.NewArgs( - filters.Arg("stars", "1"), - filters.Arg("is-official", "true"), - filters.Arg("is-automated", "true"), - ), - registryResults: []registry.SearchResult{ - { - Name: "name0", - Description: "description0", - StarCount: 0, - IsOfficial: true, - IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated). - }, - { - Name: "name1", - Description: "description1", - StarCount: 1, - IsOfficial: false, - IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated). - }, - { - Name: "name2", - Description: "description2", - StarCount: 1, - IsOfficial: true, - }, - { - Name: "name3", - Description: "description3", - StarCount: 2, - IsOfficial: true, - IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated). - }, - }, - expectedResults: []registry.SearchResult{}, - }, - } - for _, tc := range successCases { - t.Run(tc.name, func(t *testing.T) { - 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) - results, err := reg.Search(context.Background(), tc.filtersArgs, searchTerm, 0, nil, map[string][]string{}) - assert.NilError(t, err) - assert.DeepEqual(t, results, tc.expectedResults) - }) - } -} diff --git a/internal/registry/service.go b/internal/registry/service.go index 73c9edfcd1..0b6052cb5e 100644 --- a/internal/registry/service.go +++ b/internal/registry/service.go @@ -6,11 +6,9 @@ import ( "errors" "net/url" "strings" - "sync" cerrdefs "github.com/containerd/errdefs" "github.com/containerd/log" - "github.com/distribution/reference" "github.com/docker/docker/api/types/registry" ) @@ -18,7 +16,6 @@ import ( // of mirrors. type Service struct { config *serviceConfig - mu sync.RWMutex } // NewService returns a new instance of [Service] ready to be installed into @@ -32,27 +29,6 @@ func NewService(options ServiceOptions) (*Service, error) { return &Service{config: config}, err } -// ServiceConfig returns a copy of the public registry service's configuration. -func (s *Service) ServiceConfig() *registry.ServiceConfig { - s.mu.RLock() - defer s.mu.RUnlock() - return s.config.copy() -} - -// ReplaceConfig prepares a transaction which will atomically replace the -// registry service's configuration when the returned commit function is called. -func (s *Service) ReplaceConfig(options ServiceOptions) (commit func(), _ error) { - config, err := newServiceConfig(options) - if err != nil { - return nil, err - } - return func() { - s.mu.Lock() - defer s.mu.Unlock() - s.config = config - }, nil -} - // 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. @@ -74,9 +50,7 @@ func (s *Service) Auth(ctx context.Context, authConfig *registry.AuthConfig, use // Lookup endpoints for authentication but exclude mirrors to prevent // sending credentials of the upstream registry to a mirror. - s.mu.RLock() endpoints, err := s.lookupV2Endpoints(ctx, registryHostName, false) - s.mu.RUnlock() if err != nil { if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { return "", "", err @@ -108,24 +82,6 @@ func (s *Service) Auth(ctx context.Context, authConfig *registry.AuthConfig, use return "", "", lastErr } -// ResolveAuthConfig looks up authentication for the given reference from the -// given authConfigs. -// -// IMPORTANT: This function is for internal use and should not be used by external projects. -func (s *Service) ResolveAuthConfig(authConfigs map[string]registry.AuthConfig, ref reference.Named) registry.AuthConfig { - s.mu.RLock() - defer s.mu.RUnlock() - // Simplified version of "newIndexInfo" without handling of insecure - // registries and mirrors, as we don't need that information to resolve - // the auth-config. - indexName := normalizeIndexName(reference.Domain(ref)) - registryInfo, ok := s.config.IndexConfigs[indexName] - if !ok { - registryInfo = ®istry.IndexInfo{Name: indexName} - } - return ResolveAuthConfig(authConfigs, registryInfo) -} - // APIEndpoint represents a remote API endpoint type APIEndpoint struct { Mirror bool @@ -136,25 +92,11 @@ 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 *Service) LookupPullEndpoints(hostname string) ([]APIEndpoint, error) { - s.mu.RLock() - defer s.mu.RUnlock() - return s.lookupV2Endpoints(context.TODO(), hostname, true) } // 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 *Service) LookupPushEndpoints(hostname string) ([]APIEndpoint, error) { - s.mu.RLock() - defer s.mu.RUnlock() - return s.lookupV2Endpoints(context.TODO(), hostname, false) } - -// IsInsecureRegistry returns true if the registry at given host is configured as -// insecure registry. -func (s *Service) IsInsecureRegistry(host string) bool { - s.mu.RLock() - defer s.mu.RUnlock() - return !s.config.isSecureIndex(host) -}