internal/registry: remove dead code

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 7716219e17)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn 2025-07-24 01:36:57 +02:00
parent 8b9baffdf7
commit ecd54bc6dd
No known key found for this signature in database
GPG Key ID: 76698F39D527CE8C
12 changed files with 18 additions and 1578 deletions

View File

@ -127,44 +127,6 @@ func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifi
}, nil }, 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 // PingResponseError is used when the response from a ping
// was received but invalid. // was received but invalid.
type PingResponseError struct { type PingResponseError struct {

View File

@ -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 := &registry.IndexInfo{
Official: true,
}
privateIndex := &registry.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 := &registry.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)
}
}
}
}

View File

@ -106,19 +106,6 @@ func newServiceConfig(options ServiceOptions) (*serviceConfig, error) {
return config, nil 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 &registry.ServiceConfig{
InsecureRegistryCIDRs: append([]*registry.NetIPNet(nil), config.InsecureRegistryCIDRs...),
IndexConfigs: ic,
Mirrors: append([]string(nil), config.Mirrors...),
}
}
// loadMirrors loads mirrors to config, after removing duplicates. // loadMirrors loads mirrors to config, after removing duplicates.
// Returns an error if mirrors contains an invalid mirror. // Returns an error if mirrors contains an invalid mirror.
func (config *serviceConfig) loadMirrors(mirrors []string) error { func (config *serviceConfig) loadMirrors(mirrors []string) error {
@ -320,18 +307,12 @@ func ValidateIndexName(val string) (string, error) {
} }
func normalizeIndexName(val string) string { 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" { if val == "index.docker.io" {
return "docker.io" return "docker.io"
} }
return val return val
} }
func hasScheme(reposName string) bool {
return strings.Contains(reposName, "://")
}
func validateHostPort(s string) error { func validateHostPort(s string) error {
// Split host and port, and in case s can not be split, assume host only // Split host and port, and in case s can not be split, assume host only
host, port, err := net.SplitHostPort(s) host, port, err := net.SplitHostPort(s)
@ -356,32 +337,6 @@ func validateHostPort(s string) error {
return nil 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 &registry.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 // ParseRepositoryInfo performs the breakdown of a repository name into a
// [RepositoryInfo], but lacks registry configuration. // [RepositoryInfo], but lacks registry configuration.
// //

View File

@ -49,19 +49,3 @@ func (invalidParameterErr) InvalidParameter() {}
func (e invalidParameterErr) Unwrap() error { func (e invalidParameterErr) Unwrap() error {
return e.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
}

View File

@ -13,10 +13,7 @@ import (
"gotest.tools/v3/assert" "gotest.tools/v3/assert"
) )
var ( var testHTTPServer *httptest.Server
testHTTPServer *httptest.Server
testHTTPSServer *httptest.Server
)
func init() { func init() {
r := http.NewServeMux() r := http.NewServeMux()
@ -29,7 +26,6 @@ func init() {
r.HandleFunc("/v2/version", handlerGetPing) r.HandleFunc("/v2/version", handlerGetPing)
testHTTPServer = httptest.NewServer(handlerAccessLog(r)) testHTTPServer = httptest.NewServer(handlerAccessLog(r))
testHTTPSServer = httptest.NewTLSServer(handlerAccessLog(r))
} }
func handlerAccessLog(handler http.Handler) http.Handler { func handlerAccessLog(handler http.Handler) http.Handler {
@ -44,30 +40,6 @@ func makeURL(req string) string {
return testHTTPServer.URL + req return testHTTPServer.URL + req
} }
func makeHTTPSURL(req string) string {
return testHTTPSServer.URL + req
}
func makeIndex(req string) *registry.IndexInfo {
return &registry.IndexInfo{
Name: makeURL(req),
}
}
func makeHTTPSIndex(req string) *registry.IndexInfo {
return &registry.IndexInfo{
Name: makeHTTPSURL(req),
}
}
func makePublicIndex() *registry.IndexInfo {
return &registry.IndexInfo{
Name: IndexServer,
Secure: true,
Official: true,
}
}
func writeHeaders(w http.ResponseWriter) { func writeHeaders(w http.ResponseWriter) {
h := w.Header() h := w.Header()
h.Add("Server", "docker-tests/mock") h.Add("Server", "docker-tests/mock")

View File

@ -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 &registry.IndexInfo{
Name: indexName,
Mirrors: []string{},
Secure: config.isSecureIndex(indexName),
}
}
func TestParseRepositoryInfo(t *testing.T) { func TestParseRepositoryInfo(t *testing.T) {
type staticRepositoryInfo struct { type staticRepositoryInfo struct {
Index *registry.IndexInfo Index *registry.IndexInfo

View File

@ -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 &registry.IndexInfo{
Name: IndexName,
Mirrors: []string{},
Secure: true,
Official: true,
}, nil
}
return &registry.IndexInfo{
Name: indexName,
Mirrors: []string{},
Secure: !isInsecure(indexName),
}, nil
}

View File

@ -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
}

View File

@ -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 := &registry.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(), &registry.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'")
}
}
}

View File

@ -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 := &registry.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
}

View File

@ -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 := &registry.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)
})
}
}

View File

@ -6,11 +6,9 @@ import (
"errors" "errors"
"net/url" "net/url"
"strings" "strings"
"sync"
cerrdefs "github.com/containerd/errdefs" cerrdefs "github.com/containerd/errdefs"
"github.com/containerd/log" "github.com/containerd/log"
"github.com/distribution/reference"
"github.com/docker/docker/api/types/registry" "github.com/docker/docker/api/types/registry"
) )
@ -18,7 +16,6 @@ import (
// of mirrors. // of mirrors.
type Service struct { type Service struct {
config *serviceConfig config *serviceConfig
mu sync.RWMutex
} }
// NewService returns a new instance of [Service] ready to be installed into // 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 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, // Auth contacts the public registry with the provided credentials,
// and returns OK if authentication was successful. // and returns OK if authentication was successful.
// It can be used to verify the validity of a client's credentials. // 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 // Lookup endpoints for authentication but exclude mirrors to prevent
// sending credentials of the upstream registry to a mirror. // sending credentials of the upstream registry to a mirror.
s.mu.RLock()
endpoints, err := s.lookupV2Endpoints(ctx, registryHostName, false) endpoints, err := s.lookupV2Endpoints(ctx, registryHostName, false)
s.mu.RUnlock()
if err != nil { if err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return "", "", err return "", "", err
@ -108,24 +82,6 @@ func (s *Service) Auth(ctx context.Context, authConfig *registry.AuthConfig, use
return "", "", lastErr 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 = &registry.IndexInfo{Name: indexName}
}
return ResolveAuthConfig(authConfigs, registryInfo)
}
// APIEndpoint represents a remote API endpoint // APIEndpoint represents a remote API endpoint
type APIEndpoint struct { type APIEndpoint struct {
Mirror bool 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. // 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. // It gives preference to mirrors over the actual registry, and HTTPS over plain HTTP.
func (s *Service) LookupPullEndpoints(hostname string) ([]APIEndpoint, error) { func (s *Service) LookupPullEndpoints(hostname string) ([]APIEndpoint, error) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.lookupV2Endpoints(context.TODO(), hostname, true) return s.lookupV2Endpoints(context.TODO(), hostname, true)
} }
// LookupPushEndpoints creates a list of v2 endpoints to try to push to, in order of preference. // 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. // It gives preference to HTTPS over plain HTTP. Mirrors are not included.
func (s *Service) LookupPushEndpoints(hostname string) ([]APIEndpoint, error) { func (s *Service) LookupPushEndpoints(hostname string) ([]APIEndpoint, error) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.lookupV2Endpoints(context.TODO(), hostname, false) 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)
}