mirror of https://github.com/docker/cli.git
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:
parent
8b9baffdf7
commit
ecd54bc6dd
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
//
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue