Merge pull request #43298 from thaJeztah/cleanup_registry

registry: remove v1 leftovers, and refactor to reduce public api/interface
This commit is contained in:
Sebastiaan van Stijn 2022-03-17 18:58:31 +01:00 committed by GitHub
commit 7cba4ffa30
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 357 additions and 768 deletions

View file

@ -183,10 +183,8 @@ func (daemon *Daemon) RegistryHosts() docker.RegistryHosts {
}
for k, v := range m {
if d, err := registry.HostCertsDir(k); err == nil {
v.TLSConfigDir = []string{d}
m[k] = v
}
v.TLSConfigDir = []string{registry.HostCertsDir(k)}
m[k] = v
}
certsDir := registry.CertsDir()

View file

@ -11,16 +11,15 @@ import (
"github.com/docker/docker/registry"
)
type FakeService struct {
registry.DefaultService
type fakeService struct {
registry.Service
shouldReturnError bool
term string
results []registrytypes.SearchResult
}
func (s *FakeService) Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) {
func (s *fakeService) Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) {
if s.shouldReturnError {
return nil, errors.New("Search unknown error")
}
@ -76,7 +75,7 @@ func TestSearchRegistryForImagesErrors(t *testing.T) {
}
for index, e := range errorCases {
daemon := &ImageService{
registryService: &FakeService{
registryService: &fakeService{
shouldReturnError: e.shouldReturnError,
},
}
@ -322,7 +321,7 @@ func TestSearchRegistryForImages(t *testing.T) {
}
for index, s := range successCases {
daemon := &ImageService{
registryService: &FakeService{
registryService: &fakeService{
term: term,
results: s.registryResults,
},

View file

@ -10,15 +10,13 @@ import (
"github.com/docker/distribution/registry/client/auth/challenge"
"github.com/docker/distribution/registry/client/transport"
"github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/registry"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const (
// AuthClientID is used the ClientID used for the token server
AuthClientID = "docker"
)
// AuthClientID is used the ClientID used for the token server
const AuthClientID = "docker"
type loginCredentialStore struct {
authConfig *types.AuthConfig
@ -80,7 +78,7 @@ func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent strin
var (
endpointStr = strings.TrimRight(endpoint.URL.String(), "/") + "/v2/"
modifiers = Headers(userAgent, nil)
authTransport = transport.NewTransport(NewTransport(endpoint.TLSConfig), modifiers...)
authTransport = transport.NewTransport(newTransport(endpoint.TLSConfig), modifiers...)
credentialAuthConfig = *authConfig
creds = loginCredentialStore{authConfig: &credentialAuthConfig}
)
@ -109,8 +107,7 @@ func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent strin
}
// TODO(dmcgowan): Attempt to further interpret result, status code and error code string
err = errors.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode))
return "", "", err
return "", "", errors.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode))
}
func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifiers []transport.RequestModifier, creds auth.CredentialStore, scopes []auth.Scope) (*http.Client, error) {
@ -129,10 +126,9 @@ func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifi
tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions)
basicHandler := auth.NewBasicHandler(creds)
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
tr := transport.NewTransport(authTransport, modifiers...)
return &http.Client{
Transport: tr,
Transport: transport.NewTransport(authTransport, modifiers...),
Timeout: 15 * time.Second,
}, nil
}
@ -146,14 +142,11 @@ func ConvertToHostname(url string) string {
} else if strings.HasPrefix(url, "https://") {
stripped = strings.TrimPrefix(url, "https://")
}
nameParts := strings.SplitN(stripped, "/", 2)
return nameParts[0]
return strings.SplitN(stripped, "/", 2)[0]
}
// ResolveAuthConfig matches an auth configuration to a server address or a URL
func ResolveAuthConfig(authConfigs map[string]types.AuthConfig, index *registrytypes.IndexInfo) types.AuthConfig {
func ResolveAuthConfig(authConfigs map[string]types.AuthConfig, index *registry.IndexInfo) types.AuthConfig {
configKey := GetAuthConfigKey(index)
// First try the happy case
if c, found := authConfigs[configKey]; found || index.Official {

View file

@ -4,14 +4,15 @@ import (
"testing"
"github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/registry"
"gotest.tools/v3/assert"
)
func buildAuthConfigs() map[string]types.AuthConfig {
authConfigs := map[string]types.AuthConfig{}
for _, registry := range []string{"testIndex", IndexServer} {
authConfigs[registry] = types.AuthConfig{
for _, reg := range []string{"testIndex", IndexServer} {
authConfigs[reg] = types.AuthConfig{
Username: "docker-user",
Password: "docker-pass",
}
@ -24,18 +25,18 @@ func TestResolveAuthConfigIndexServer(t *testing.T) {
authConfigs := buildAuthConfigs()
indexConfig := authConfigs[IndexServer]
officialIndex := &registrytypes.IndexInfo{
officialIndex := &registry.IndexInfo{
Official: true,
}
privateIndex := &registrytypes.IndexInfo{
privateIndex := &registry.IndexInfo{
Official: false,
}
resolved := ResolveAuthConfig(authConfigs, officialIndex)
assertEqual(t, resolved, indexConfig, "Expected ResolveAuthConfig to return IndexServer")
assert.Equal(t, resolved, indexConfig, "Expected ResolveAuthConfig to return IndexServer")
resolved = ResolveAuthConfig(authConfigs, privateIndex)
assertNotEqual(t, resolved, indexConfig, "Expected ResolveAuthConfig to not return IndexServer")
assert.Check(t, resolved != indexConfig, "Expected ResolveAuthConfig to not return IndexServer")
}
func TestResolveAuthConfigFullURL(t *testing.T) {
@ -87,19 +88,19 @@ func TestResolveAuthConfigFullURL(t *testing.T) {
if !ok {
t.Fail()
}
index := &registrytypes.IndexInfo{
index := &registry.IndexInfo{
Name: configKey,
}
for _, registry := range registries {
authConfigs[registry] = configured
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", registry, resolved, configured)
t.Errorf("%s -> %v != %v\n", reg, resolved, configured)
}
delete(authConfigs, registry)
delete(authConfigs, reg)
resolved = ResolveAuthConfig(authConfigs, index)
if resolved.Username == configured.Username || resolved.Password == configured.Password {
t.Errorf("%s -> %v == %v\n", registry, resolved, configured)
t.Errorf("%s -> %v == %v\n", reg, resolved, configured)
}
}
}

View file

@ -1,7 +1,6 @@
package registry // import "github.com/docker/docker/registry"
import (
"fmt"
"net"
"net/url"
"regexp"
@ -9,8 +8,7 @@ import (
"strings"
"github.com/docker/distribution/reference"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/pkg/errors"
"github.com/docker/docker/api/types/registry"
"github.com/sirupsen/logrus"
)
@ -22,9 +20,7 @@ type ServiceOptions struct {
}
// serviceConfig holds daemon configuration for the registry service.
type serviceConfig struct {
registrytypes.ServiceConfig
}
type serviceConfig registry.ServiceConfig
// TODO(thaJeztah) both the "index.docker.io" and "registry-1.docker.io" domains
// are here for historic reasons and backward-compatibility. These domains
@ -58,10 +54,6 @@ var (
Host: DefaultRegistryHost,
}
// ErrInvalidRepositoryName is an error returned if the repository name did
// not have the correct form
ErrInvalidRepositoryName = errors.New("Invalid repository name (ex: \"registry.domain.tld/myrepos\")")
emptyServiceConfig, _ = newServiceConfig(ServiceOptions{})
validHostPortRegex = regexp.MustCompile(`^` + reference.DomainRegexp.String() + `$`)
@ -71,57 +63,65 @@ var (
// newServiceConfig returns a new instance of ServiceConfig
func newServiceConfig(options ServiceOptions) (*serviceConfig, error) {
config := &serviceConfig{
ServiceConfig: registrytypes.ServiceConfig{
InsecureRegistryCIDRs: make([]*registrytypes.NetIPNet, 0),
IndexConfigs: make(map[string]*registrytypes.IndexInfo),
// Hack: Bypass setting the mirrors to IndexConfigs since they are going away
// and Mirrors are only for the official registry anyways.
},
}
if err := config.LoadAllowNondistributableArtifacts(options.AllowNondistributableArtifacts); err != nil {
config := &serviceConfig{}
if err := config.loadAllowNondistributableArtifacts(options.AllowNondistributableArtifacts); err != nil {
return nil, err
}
if err := config.LoadMirrors(options.Mirrors); err != nil {
if err := config.loadMirrors(options.Mirrors); err != nil {
return nil, err
}
if err := config.LoadInsecureRegistries(options.InsecureRegistries); err != nil {
if err := config.loadInsecureRegistries(options.InsecureRegistries); err != nil {
return nil, err
}
return config, nil
}
// LoadAllowNondistributableArtifacts loads allow-nondistributable-artifacts registries into config.
func (config *serviceConfig) LoadAllowNondistributableArtifacts(registries []string) error {
cidrs := map[string]*registrytypes.NetIPNet{}
// 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{
AllowNondistributableArtifactsCIDRs: append([]*registry.NetIPNet(nil), config.AllowNondistributableArtifactsCIDRs...),
AllowNondistributableArtifactsHostnames: append([]string(nil), config.AllowNondistributableArtifactsHostnames...),
InsecureRegistryCIDRs: append([]*registry.NetIPNet(nil), config.InsecureRegistryCIDRs...),
IndexConfigs: ic,
Mirrors: append([]string(nil), config.Mirrors...),
}
}
// loadAllowNondistributableArtifacts loads allow-nondistributable-artifacts registries into config.
func (config *serviceConfig) loadAllowNondistributableArtifacts(registries []string) error {
cidrs := map[string]*registry.NetIPNet{}
hostnames := map[string]bool{}
for _, r := range registries {
if _, err := ValidateIndexName(r); err != nil {
return err
}
if validateNoScheme(r) != nil {
return fmt.Errorf("allow-nondistributable-artifacts registry %s should not contain '://'", r)
if hasScheme(r) {
return invalidParamf("allow-nondistributable-artifacts registry %s should not contain '://'", r)
}
if _, ipnet, err := net.ParseCIDR(r); err == nil {
// Valid CIDR.
cidrs[ipnet.String()] = (*registrytypes.NetIPNet)(ipnet)
} else if err := validateHostPort(r); err == nil {
cidrs[ipnet.String()] = (*registry.NetIPNet)(ipnet)
} else if err = validateHostPort(r); err == nil {
// Must be `host:port` if not CIDR.
hostnames[r] = true
} else {
return fmt.Errorf("allow-nondistributable-artifacts registry %s is not valid: %v", r, err)
return invalidParamWrapf(err, "allow-nondistributable-artifacts registry %s is not valid", r)
}
}
config.AllowNondistributableArtifactsCIDRs = make([]*(registrytypes.NetIPNet), 0)
config.AllowNondistributableArtifactsCIDRs = make([]*registry.NetIPNet, 0, len(cidrs))
for _, c := range cidrs {
config.AllowNondistributableArtifactsCIDRs = append(config.AllowNondistributableArtifactsCIDRs, c)
}
config.AllowNondistributableArtifactsHostnames = make([]string, 0)
config.AllowNondistributableArtifactsHostnames = make([]string, 0, len(hostnames))
for h := range hostnames {
config.AllowNondistributableArtifactsHostnames = append(config.AllowNondistributableArtifactsHostnames, h)
}
@ -129,9 +129,9 @@ func (config *serviceConfig) LoadAllowNondistributableArtifacts(registries []str
return nil
}
// 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.
func (config *serviceConfig) LoadMirrors(mirrors []string) error {
func (config *serviceConfig) loadMirrors(mirrors []string) error {
mMap := map[string]struct{}{}
unique := []string{}
@ -149,40 +149,33 @@ func (config *serviceConfig) LoadMirrors(mirrors []string) error {
config.Mirrors = unique
// Configure public registry since mirrors may have changed.
config.IndexConfigs[IndexName] = &registrytypes.IndexInfo{
Name: IndexName,
Mirrors: config.Mirrors,
Secure: true,
Official: true,
config.IndexConfigs = map[string]*registry.IndexInfo{
IndexName: {
Name: IndexName,
Mirrors: unique,
Secure: true,
Official: true,
},
}
return nil
}
// LoadInsecureRegistries loads insecure registries to config
func (config *serviceConfig) LoadInsecureRegistries(registries []string) error {
// Localhost is by default considered as an insecure registry
// This is a stop-gap for people who are running a private registry on localhost (especially on Boot2docker).
//
// TODO: should we deprecate this once it is easier for people to set up a TLS registry or change
// daemon flags on boot2docker?
// loadInsecureRegistries loads insecure registries to config
func (config *serviceConfig) loadInsecureRegistries(registries []string) error {
// Localhost is by default considered as an insecure registry. This is a
// stop-gap for people who are running a private registry on localhost.
registries = append(registries, "127.0.0.0/8")
// Store original InsecureRegistryCIDRs and IndexConfigs
// Clean InsecureRegistryCIDRs and IndexConfigs in config, as passed registries has all insecure registry info.
originalCIDRs := config.ServiceConfig.InsecureRegistryCIDRs
originalIndexInfos := config.ServiceConfig.IndexConfigs
config.ServiceConfig.InsecureRegistryCIDRs = make([]*registrytypes.NetIPNet, 0)
config.ServiceConfig.IndexConfigs = make(map[string]*registrytypes.IndexInfo)
var (
insecureRegistryCIDRs = make([]*registry.NetIPNet, 0)
indexConfigs = make(map[string]*registry.IndexInfo)
)
skip:
for _, r := range registries {
// validate insecure registry
if _, err := ValidateIndexName(r); err != nil {
// before returning err, roll back to original data
config.ServiceConfig.InsecureRegistryCIDRs = originalCIDRs
config.ServiceConfig.IndexConfigs = originalIndexInfos
return err
}
if strings.HasPrefix(strings.ToLower(r), "http://") {
@ -191,35 +184,27 @@ skip:
} else if strings.HasPrefix(strings.ToLower(r), "https://") {
logrus.Warnf("insecure registry %s should not contain 'https://' and 'https://' has been removed from the insecure registry config", r)
r = r[8:]
} else if validateNoScheme(r) != nil {
// Insecure registry should not contain '://'
// before returning err, roll back to original data
config.ServiceConfig.InsecureRegistryCIDRs = originalCIDRs
config.ServiceConfig.IndexConfigs = originalIndexInfos
return fmt.Errorf("insecure registry %s should not contain '://'", r)
} else if hasScheme(r) {
return invalidParamf("insecure registry %s should not contain '://'", r)
}
// Check if CIDR was passed to --insecure-registry
_, ipnet, err := net.ParseCIDR(r)
if err == nil {
// Valid CIDR. If ipnet is already in config.InsecureRegistryCIDRs, skip.
data := (*registrytypes.NetIPNet)(ipnet)
for _, value := range config.InsecureRegistryCIDRs {
data := (*registry.NetIPNet)(ipnet)
for _, value := range insecureRegistryCIDRs {
if value.IP.String() == data.IP.String() && value.Mask.String() == data.Mask.String() {
continue skip
}
}
// ipnet is not found, add it in config.InsecureRegistryCIDRs
config.InsecureRegistryCIDRs = append(config.InsecureRegistryCIDRs, data)
insecureRegistryCIDRs = append(insecureRegistryCIDRs, data)
} else {
if err := validateHostPort(r); err != nil {
config.ServiceConfig.InsecureRegistryCIDRs = originalCIDRs
config.ServiceConfig.IndexConfigs = originalIndexInfos
return fmt.Errorf("insecure registry %s is not valid: %v", r, err)
return invalidParamWrapf(err, "insecure registry %s is not valid", r)
}
// Assume `host:port` if not CIDR.
config.IndexConfigs[r] = &registrytypes.IndexInfo{
indexConfigs[r] = &registry.IndexInfo{
Name: r,
Mirrors: make([]string, 0),
Secure: false,
@ -229,12 +214,14 @@ skip:
}
// Configure public registry.
config.IndexConfigs[IndexName] = &registrytypes.IndexInfo{
indexConfigs[IndexName] = &registry.IndexInfo{
Name: IndexName,
Mirrors: config.Mirrors,
Secure: true,
Official: true,
}
config.InsecureRegistryCIDRs = insecureRegistryCIDRs
config.IndexConfigs = indexConfigs
return nil
}
@ -248,7 +235,7 @@ skip:
// hostname should be a URL.Host (`host:port` or `host`) where the `host` part can be either a domain name
// or an IP address. If it is a domain name, then it will be resolved to IP addresses for matching. If
// resolution fails, CIDR matching is not performed.
func allowNondistributableArtifacts(config *serviceConfig, hostname string) bool {
func (config *serviceConfig) allowNondistributableArtifacts(hostname string) bool {
for _, h := range config.AllowNondistributableArtifactsHostnames {
if h == hostname {
return true
@ -269,7 +256,7 @@ func allowNondistributableArtifacts(config *serviceConfig, hostname string) bool
// or an IP address. If it is a domain name, then it will be resolved in order to check if the IP is contained
// in a subnet. If the resolving is not successful, isSecureIndex will only try to match hostname to any element
// of insecureRegistries.
func isSecureIndex(config *serviceConfig, indexName string) bool {
func (config *serviceConfig) isSecureIndex(indexName string) bool {
// Check for configured index, first. This is needed in case isSecureIndex
// is called from anything besides newIndexInfo, in order to honor per-index configurations.
if index, ok := config.IndexConfigs[indexName]; ok {
@ -282,7 +269,7 @@ func isSecureIndex(config *serviceConfig, indexName string) bool {
// isCIDRMatch returns true if URLHost matches an element of cidrs. URLHost is a URL.Host (`host:port` or `host`)
// where the `host` part can be either a domain name or an IP address. If it is a domain name, then it will be
// resolved to IP addresses for matching. If resolution fails, false is returned.
func isCIDRMatch(cidrs []*registrytypes.NetIPNet, URLHost string) bool {
func isCIDRMatch(cidrs []*registry.NetIPNet, URLHost string) bool {
host, _, err := net.SplitHostPort(URLHost)
if err != nil {
// Assume URLHost is of the form `host` without the port and go on.
@ -318,18 +305,18 @@ func isCIDRMatch(cidrs []*registrytypes.NetIPNet, URLHost string) bool {
func ValidateMirror(val string) (string, error) {
uri, err := url.Parse(val)
if err != nil {
return "", fmt.Errorf("invalid mirror: %q is not a valid URI", val)
return "", invalidParamWrapf(err, "invalid mirror: %q is not a valid URI", val)
}
if uri.Scheme != "http" && uri.Scheme != "https" {
return "", fmt.Errorf("invalid mirror: unsupported scheme %q in %q", uri.Scheme, uri)
return "", invalidParamf("invalid mirror: unsupported scheme %q in %q", uri.Scheme, uri)
}
if (uri.Path != "" && uri.Path != "/") || uri.RawQuery != "" || uri.Fragment != "" {
return "", fmt.Errorf("invalid mirror: path, query, or fragment at end of the URI %q", uri)
return "", invalidParamf("invalid mirror: path, query, or fragment at end of the URI %q", uri)
}
if uri.User != nil {
// strip password from output
uri.User = url.UserPassword(uri.User.Username(), "xxxxx")
return "", fmt.Errorf("invalid mirror: username/password not allowed in URI %q", uri)
return "", invalidParamf("invalid mirror: username/password not allowed in URI %q", uri)
}
return strings.TrimSuffix(val, "/") + "/", nil
}
@ -341,17 +328,13 @@ func ValidateIndexName(val string) (string, error) {
val = "docker.io"
}
if strings.HasPrefix(val, "-") || strings.HasSuffix(val, "-") {
return "", fmt.Errorf("invalid index name (%s). Cannot begin or end with a hyphen", val)
return "", invalidParamf("invalid index name (%s). Cannot begin or end with a hyphen", val)
}
return val, nil
}
func validateNoScheme(reposName string) error {
if strings.Contains(reposName, "://") {
// It cannot contain a scheme!
return ErrInvalidRepositoryName
}
return nil
func hasScheme(reposName string) bool {
return strings.Contains(reposName, "://")
}
func validateHostPort(s string) error {
@ -364,7 +347,7 @@ func validateHostPort(s string) error {
// If match against the `host:port` pattern fails,
// it might be `IPv6:port`, which will be captured by net.ParseIP(host)
if !validHostPortRegex.MatchString(s) && net.ParseIP(host) == nil {
return fmt.Errorf("invalid host %q", host)
return invalidParamf("invalid host %q", host)
}
if port != "" {
v, err := strconv.Atoi(port)
@ -372,14 +355,14 @@ func validateHostPort(s string) error {
return err
}
if v < 0 || v > 65535 {
return fmt.Errorf("invalid port %q", port)
return invalidParamf("invalid port %q", port)
}
}
return nil
}
// newIndexInfo returns IndexInfo configuration from indexName
func newIndexInfo(config *serviceConfig, indexName string) (*registrytypes.IndexInfo, error) {
func newIndexInfo(config *serviceConfig, indexName string) (*registry.IndexInfo, error) {
var err error
indexName, err = ValidateIndexName(indexName)
if err != nil {
@ -392,18 +375,17 @@ func newIndexInfo(config *serviceConfig, indexName string) (*registrytypes.Index
}
// Construct a non-configured index info.
index := &registrytypes.IndexInfo{
return &registry.IndexInfo{
Name: indexName,
Mirrors: make([]string, 0),
Secure: config.isSecureIndex(indexName),
Official: false,
}
index.Secure = isSecureIndex(config, indexName)
return index, nil
}, nil
}
// 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 *registrytypes.IndexInfo) string {
func GetAuthConfigKey(index *registry.IndexInfo) string {
if index.Official {
return IndexServer
}
@ -432,7 +414,12 @@ func ParseRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) {
}
// ParseSearchIndexInfo will use repository name to get back an indexInfo.
func ParseSearchIndexInfo(reposName string) (*registrytypes.IndexInfo, error) {
//
// 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)
indexInfo, err := newIndexInfo(emptyServiceConfig, indexName)

View file

@ -6,6 +6,7 @@ import (
"strings"
"testing"
"github.com/docker/docker/errdefs"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
@ -94,7 +95,7 @@ func TestLoadAllowNondistributableArtifacts(t *testing.T) {
}
for _, testCase := range testCases {
config := emptyServiceConfig
err := config.LoadAllowNondistributableArtifacts(testCase.registries)
err := config.loadAllowNondistributableArtifacts(testCase.registries)
if testCase.err == "" {
if err != nil {
t.Fatalf("expect no error, got '%s'", err)
@ -237,7 +238,7 @@ func TestLoadInsecureRegistries(t *testing.T) {
}
for _, testCase := range testCases {
config := emptyServiceConfig
err := config.LoadInsecureRegistries(testCase.registries)
err := config.loadInsecureRegistries(testCase.registries)
if testCase.err == "" {
if err != nil {
t.Fatalf("expect no error, got '%s'", err)
@ -255,9 +256,8 @@ func TestLoadInsecureRegistries(t *testing.T) {
if err == nil {
t.Fatalf("expect error '%s', got no error", testCase.err)
}
if !strings.Contains(err.Error(), testCase.err) {
t.Fatalf("expect error '%s', got '%s'", testCase.err, err)
}
assert.ErrorContains(t, err, testCase.err)
assert.Check(t, errdefs.IsInvalidParameter(err))
}
}
}
@ -313,6 +313,7 @@ func TestNewServiceConfig(t *testing.T) {
_, err := newServiceConfig(testCase.opts)
if testCase.errStr != "" {
assert.Check(t, is.Error(err, testCase.errStr))
assert.Check(t, errdefs.IsInvalidParameter(err))
} else {
assert.Check(t, err)
}
@ -377,5 +378,6 @@ func TestValidateIndexNameWithError(t *testing.T) {
for _, testCase := range invalid {
_, err := ValidateIndexName(testCase.index)
assert.Check(t, is.Error(err, testCase.err))
assert.Check(t, errdefs.IsInvalidParameter(err))
}
}

View file

@ -63,9 +63,9 @@ func TestValidateEndpoint(t *testing.T) {
t.Fatal(err)
}
testEndpoint := V1Endpoint{
testEndpoint := v1Endpoint{
URL: testServerURL,
client: HTTPClient(NewTransport(nil)),
client: httpClient(newTransport(nil)),
}
if err = validateEndpoint(&testEndpoint); err != nil {

View file

@ -3,27 +3,39 @@ package registry // import "github.com/docker/docker/registry"
import (
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/docker/distribution/registry/client/transport"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/registry"
"github.com/sirupsen/logrus"
)
// V1Endpoint stores basic information about a V1 registry endpoint.
type V1Endpoint struct {
// v1PingResult contains the information returned when pinging a registry. It
// indicates the registry's version and whether the registry claims to be a
// standalone registry.
type v1PingResult struct {
// Version is the registry version supplied by the registry in an HTTP
// header
Version string `json:"version"`
// 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.
// newV1Endpoint parses the given address to return a registry endpoint.
// TODO: remove. This is only used by search.
func NewV1Endpoint(index *registrytypes.IndexInfo, userAgent string, metaHeaders http.Header) (*V1Endpoint, error) {
func newV1Endpoint(index *registry.IndexInfo, userAgent string, metaHeaders http.Header) (*v1Endpoint, error) {
tlsConfig, err := newTLSConfig(index.Name, index.Secure)
if err != nil {
return nil, err
@ -42,28 +54,28 @@ func NewV1Endpoint(index *registrytypes.IndexInfo, userAgent string, metaHeaders
return endpoint, nil
}
func validateEndpoint(endpoint *V1Endpoint) error {
func validateEndpoint(endpoint *v1Endpoint) error {
logrus.Debugf("pinging registry endpoint %s", endpoint)
// Try HTTPS ping to registry
endpoint.URL.Scheme = "https"
if _, err := endpoint.Ping(); err != nil {
if _, err := endpoint.ping(); err != nil {
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 fallback to HTTP.
return fmt.Errorf("invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %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; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host)
return invalidParamf("invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %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; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host)
}
// If registry is insecure and HTTPS failed, fallback to HTTP.
logrus.Debugf("Error from registry %q marked as insecure: %v. Insecurely falling back to HTTP", endpoint, err)
logrus.WithError(err).Debugf("error from registry %q marked as insecure - insecurely falling back to HTTP", endpoint)
endpoint.URL.Scheme = "http"
var err2 error
if _, err2 = endpoint.Ping(); err2 == nil {
if _, err2 = endpoint.ping(); err2 == nil {
return nil
}
return fmt.Errorf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2)
return invalidParamf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2)
}
return nil
@ -72,28 +84,23 @@ func validateEndpoint(endpoint *V1Endpoint) error {
// trimV1Address trims the version off the address and returns the
// trimmed address or an error if there is a non-V1 version.
func trimV1Address(address string) (string, error) {
var (
chunks []string
apiVersionStr string
)
address = strings.TrimSuffix(address, "/")
chunks = strings.Split(address, "/")
apiVersionStr = chunks[len(chunks)-1]
chunks := strings.Split(address, "/")
apiVersionStr := chunks[len(chunks)-1]
if apiVersionStr == "v1" {
return strings.Join(chunks[:len(chunks)-1], "/"), nil
}
for k, v := range apiVersions {
if k != APIVersion1 && apiVersionStr == v {
return "", fmt.Errorf("unsupported V1 version path %s", apiVersionStr)
return "", invalidParamf("unsupported V1 version path %s", apiVersionStr)
}
}
return address, nil
}
func newV1EndpointFromStr(address string, tlsConfig *tls.Config, userAgent string, metaHeaders http.Header) (*V1Endpoint, error) {
func newV1EndpointFromStr(address string, tlsConfig *tls.Config, userAgent string, metaHeaders http.Header) (*v1Endpoint, error) {
if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
address = "https://" + address
}
@ -105,69 +112,64 @@ func newV1EndpointFromStr(address string, tlsConfig *tls.Config, userAgent strin
uri, err := url.Parse(address)
if err != nil {
return nil, err
return nil, invalidParam(err)
}
// TODO(tiborvass): make sure a ConnectTimeout transport is used
tr := NewTransport(tlsConfig)
tr := newTransport(tlsConfig)
return &V1Endpoint{
return &v1Endpoint{
IsSecure: tlsConfig == nil || !tlsConfig.InsecureSkipVerify,
URL: uri,
client: HTTPClient(transport.NewTransport(tr, Headers(userAgent, metaHeaders)...)),
client: httpClient(transport.NewTransport(tr, Headers(userAgent, metaHeaders)...)),
}, nil
}
// Get the formatted URL for the root of this registry Endpoint
func (e *V1Endpoint) String() string {
func (e *v1Endpoint) String() string {
return e.URL.String() + "/v1/"
}
// Path returns a formatted string for the URL
// of this endpoint with the given path appended.
func (e *V1Endpoint) Path(path string) string {
return e.URL.String() + "/v1/" + path
}
// Ping returns a PingResult which indicates whether the registry is standalone or not.
func (e *V1Endpoint) Ping() (PingResult, error) {
// ping returns a v1PingResult which indicates whether the registry is standalone or not.
func (e *v1Endpoint) ping() (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 PingResult{}, nil
return v1PingResult{}, nil
}
logrus.Debugf("attempting v1 ping for registry endpoint %s", e)
req, err := http.NewRequest(http.MethodGet, e.Path("_ping"), nil)
pingURL := e.String() + "_ping"
req, err := http.NewRequest(http.MethodGet, pingURL, nil)
if err != nil {
return PingResult{}, err
return v1PingResult{}, invalidParam(err)
}
resp, err := e.client.Do(req)
if err != nil {
return PingResult{}, err
return v1PingResult{}, invalidParam(err)
}
defer resp.Body.Close()
jsonString, err := io.ReadAll(resp.Body)
if err != nil {
return PingResult{}, fmt.Errorf("error while reading the http response: %s", err)
return v1PingResult{}, invalidParamWrapf(err, "error while reading response from %s", pingURL)
}
// If the header is absent, we assume true for compatibility with earlier
// versions of the registry. default to true
info := PingResult{
info := v1PingResult{
Standalone: true,
}
if err := json.Unmarshal(jsonString, &info); err != nil {
logrus.Debugf("Error unmarshaling the _ping PingResult: %s", err)
logrus.WithError(err).Debug("error unmarshaling _ping response")
// don't stop here. Just assume sane defaults
}
if hdr := resp.Header.Get("X-Docker-Registry-Version"); hdr != "" {
info.Version = hdr
}
logrus.Debugf("PingResult.Version: %q", info.Version)
logrus.Debugf("v1PingResult.Version: %q", info.Version)
standalone := resp.Header.Get("X-Docker-Registry-Standalone")
@ -178,6 +180,6 @@ func (e *V1Endpoint) Ping() (PingResult, error) {
// there is a header set, and it is not "true" or "1", so assume fails
info.Standalone = false
}
logrus.Debugf("PingResult.Standalone: %t", info.Standalone)
logrus.Debugf("v1PingResult.Standalone: %t", info.Standalone)
return info, nil
}

View file

@ -5,6 +5,7 @@ import (
"github.com/docker/distribution/registry/api/errcode"
"github.com/docker/docker/errdefs"
"github.com/pkg/errors"
)
func translateV2AuthError(err error) error {
@ -21,3 +22,15 @@ func translateV2AuthError(err error) error {
return err
}
func invalidParam(err error) error {
return errdefs.InvalidParameter(err)
}
func invalidParamf(format string, args ...interface{}) error {
return errdefs.InvalidParameter(errors.Errorf(format, args...))
}
func invalidParamWrapf(err error, format string, args ...interface{}) error {
return errdefs.InvalidParameter(errors.Wrapf(err, format, args...))
}

View file

@ -3,7 +3,6 @@ package registry // import "github.com/docker/docker/registry"
import (
"crypto/tls"
"fmt"
"net"
"net/http"
"os"
@ -16,15 +15,12 @@ import (
"github.com/sirupsen/logrus"
)
// HostCertsDir returns the config directory for a specific host
func HostCertsDir(hostname string) (string, error) {
certsDir := CertsDir()
hostDir := filepath.Join(certsDir, cleanPath(hostname))
return hostDir, nil
// HostCertsDir returns the config directory for a specific host.
func HostCertsDir(hostname string) string {
return filepath.Join(CertsDir(), cleanPath(hostname))
}
// newTLSConfig constructs a client TLS configuration based on server defaults
func newTLSConfig(hostname string, isSecure bool) (*tls.Config, error) {
// PreferredServerCipherSuites should have no effect
tlsConfig := tlsconfig.ServerDefault()
@ -32,11 +28,7 @@ func newTLSConfig(hostname string, isSecure bool) (*tls.Config, error) {
tlsConfig.InsecureSkipVerify = !isSecure
if isSecure && CertsDir() != "" {
hostDir, err := HostCertsDir(hostname)
if err != nil {
return nil, err
}
hostDir := HostCertsDir(hostname)
logrus.Debugf("hostDir: %s", hostDir)
if err := ReadCertsDirectory(tlsConfig, hostDir); err != nil {
return nil, err
@ -61,7 +53,7 @@ func hasFile(files []os.DirEntry, name string) bool {
func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error {
fs, err := os.ReadDir(directory)
if err != nil && !os.IsNotExist(err) {
return err
return invalidParam(err)
}
for _, f := range fs {
@ -69,7 +61,7 @@ func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error {
if tlsConfig.RootCAs == nil {
systemPool, err := tlsconfig.SystemCertPool()
if err != nil {
return fmt.Errorf("unable to get system cert pool: %v", err)
return invalidParamWrapf(err, "unable to get system cert pool")
}
tlsConfig.RootCAs = systemPool
}
@ -85,7 +77,7 @@ func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error {
keyName := certName[:len(certName)-5] + ".key"
logrus.Debugf("cert: %s", filepath.Join(directory, f.Name()))
if !hasFile(fs, keyName) {
return fmt.Errorf("missing key %s for client certificate %s. Note that CA certificates should use the extension .crt", keyName, certName)
return invalidParamf("missing key %s for client certificate %s. CA certificates must use the extension .crt", keyName, certName)
}
cert, err := tls.LoadX509KeyPair(filepath.Join(directory, certName), filepath.Join(directory, keyName))
if err != nil {
@ -98,7 +90,7 @@ func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error {
certName := keyName[:len(keyName)-4] + ".cert"
logrus.Debugf("key: %s", filepath.Join(directory, f.Name()))
if !hasFile(fs, certName) {
return fmt.Errorf("Missing client certificate %s for key %s", certName, keyName)
return invalidParamf("missing client certificate %s for key %s", certName, keyName)
}
}
}
@ -120,9 +112,9 @@ func Headers(userAgent string, metaHeaders http.Header) []transport.RequestModif
return modifiers
}
// HTTPClient returns an HTTP client structure which uses the given transport
// httpClient returns an HTTP client structure which uses the given transport
// and contains the necessary headers for redirected requests
func HTTPClient(transport http.RoundTripper) *http.Client {
func httpClient(transport http.RoundTripper) *http.Client {
return &http.Client{
Transport: transport,
CheckRedirect: addRequiredHeadersToRedirectedRequests,
@ -165,9 +157,9 @@ func addRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Reque
return nil
}
// NewTransport returns a new HTTP transport. If tlsConfig is nil, it uses the
// newTransport returns a new HTTP transport. If tlsConfig is nil, it uses the
// default TLS configuration.
func NewTransport(tlsConfig *tls.Config) *http.Transport {
func newTransport(tlsConfig *tls.Config) *http.Transport {
if tlsConfig == nil {
tlsConfig = tlsconfig.ServerDefault()
}
@ -177,7 +169,7 @@ func NewTransport(tlsConfig *tls.Config) *http.Transport {
KeepAlive: 30 * time.Second,
}
base := &http.Transport{
return &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: direct.DialContext,
TLSHandshakeTimeout: 10 * time.Second,
@ -185,6 +177,4 @@ func NewTransport(tlsConfig *tls.Config) *http.Transport {
// TODO(dmcgowan): Call close idle connections when complete and use keep alive
DisableKeepAlives: true,
}
return base
}

View file

@ -3,93 +3,21 @@ package registry // import "github.com/docker/docker/registry"
import (
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"testing"
"time"
"github.com/docker/distribution/reference"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/registry"
"github.com/gorilla/mux"
"github.com/sirupsen/logrus"
"gotest.tools/v3/assert"
)
var (
testHTTPServer *httptest.Server
testHTTPSServer *httptest.Server
testLayers = map[string]map[string]string{
"77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20": {
"json": `{"id":"77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20",
"comment":"test base image","created":"2013-03-23T12:53:11.10432-07:00",
"container_config":{"Hostname":"","User":"","Memory":0,"MemorySwap":0,
"CpuShares":0,"AttachStdin":false,"AttachStdout":false,"AttachStderr":false,
"Tty":false,"OpenStdin":false,"StdinOnce":false,
"Env":null,"Cmd":null,"Dns":null,"Image":"","Volumes":null,
"VolumesFrom":"","Entrypoint":null},"Size":424242}`,
"checksum_simple": "sha256:1ac330d56e05eef6d438586545ceff7550d3bdcb6b19961f12c5ba714ee1bb37",
"checksum_tarsum": "tarsum+sha256:4409a0685741ca86d38df878ed6f8cbba4c99de5dc73cd71aef04be3bb70be7c",
"ancestry": `["77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20"]`,
"layer": string([]byte{
0x1f, 0x8b, 0x08, 0x08, 0x0e, 0xb0, 0xee, 0x51, 0x02, 0x03, 0x6c, 0x61, 0x79, 0x65,
0x72, 0x2e, 0x74, 0x61, 0x72, 0x00, 0xed, 0xd2, 0x31, 0x0e, 0xc2, 0x30, 0x0c, 0x05,
0x50, 0xcf, 0x9c, 0xc2, 0x27, 0x48, 0xed, 0x38, 0x4e, 0xce, 0x13, 0x44, 0x2b, 0x66,
0x62, 0x24, 0x8e, 0x4f, 0xa0, 0x15, 0x63, 0xb6, 0x20, 0x21, 0xfc, 0x96, 0xbf, 0x78,
0xb0, 0xf5, 0x1d, 0x16, 0x98, 0x8e, 0x88, 0x8a, 0x2a, 0xbe, 0x33, 0xef, 0x49, 0x31,
0xed, 0x79, 0x40, 0x8e, 0x5c, 0x44, 0x85, 0x88, 0x33, 0x12, 0x73, 0x2c, 0x02, 0xa8,
0xf0, 0x05, 0xf7, 0x66, 0xf5, 0xd6, 0x57, 0x69, 0xd7, 0x7a, 0x19, 0xcd, 0xf5, 0xb1,
0x6d, 0x1b, 0x1f, 0xf9, 0xba, 0xe3, 0x93, 0x3f, 0x22, 0x2c, 0xb6, 0x36, 0x0b, 0xf6,
0xb0, 0xa9, 0xfd, 0xe7, 0x94, 0x46, 0xfd, 0xeb, 0xd1, 0x7f, 0x2c, 0xc4, 0xd2, 0xfb,
0x97, 0xfe, 0x02, 0x80, 0xe4, 0xfd, 0x4f, 0x77, 0xae, 0x6d, 0x3d, 0x81, 0x73, 0xce,
0xb9, 0x7f, 0xf3, 0x04, 0x41, 0xc1, 0xab, 0xc6, 0x00, 0x0a, 0x00, 0x00,
}),
},
"42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d": {
"json": `{"id":"42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d",
"parent":"77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20",
"comment":"test base image","created":"2013-03-23T12:55:11.10432-07:00",
"container_config":{"Hostname":"","User":"","Memory":0,"MemorySwap":0,
"CpuShares":0,"AttachStdin":false,"AttachStdout":false,"AttachStderr":false,
"Tty":false,"OpenStdin":false,"StdinOnce":false,
"Env":null,"Cmd":null,"Dns":null,"Image":"","Volumes":null,
"VolumesFrom":"","Entrypoint":null},"Size":424242}`,
"checksum_simple": "sha256:bea7bf2e4bacd479344b737328db47b18880d09096e6674165533aa994f5e9f2",
"checksum_tarsum": "tarsum+sha256:68fdb56fb364f074eec2c9b3f85ca175329c4dcabc4a6a452b7272aa613a07a2",
"ancestry": `["42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d",
"77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20"]`,
"layer": string([]byte{
0x1f, 0x8b, 0x08, 0x08, 0xbd, 0xb3, 0xee, 0x51, 0x02, 0x03, 0x6c, 0x61, 0x79, 0x65,
0x72, 0x2e, 0x74, 0x61, 0x72, 0x00, 0xed, 0xd1, 0x31, 0x0e, 0xc2, 0x30, 0x0c, 0x05,
0x50, 0xcf, 0x9c, 0xc2, 0x27, 0x48, 0x9d, 0x38, 0x8e, 0xcf, 0x53, 0x51, 0xaa, 0x56,
0xea, 0x44, 0x82, 0xc4, 0xf1, 0x09, 0xb4, 0xea, 0x98, 0x2d, 0x48, 0x08, 0xbf, 0xe5,
0x2f, 0x1e, 0xfc, 0xf5, 0xdd, 0x00, 0xdd, 0x11, 0x91, 0x8a, 0xe0, 0x27, 0xd3, 0x9e,
0x14, 0xe2, 0x9e, 0x07, 0xf4, 0xc1, 0x2b, 0x0b, 0xfb, 0xa4, 0x82, 0xe4, 0x3d, 0x93,
0x02, 0x0a, 0x7c, 0xc1, 0x23, 0x97, 0xf1, 0x5e, 0x5f, 0xc9, 0xcb, 0x38, 0xb5, 0xee,
0xea, 0xd9, 0x3c, 0xb7, 0x4b, 0xbe, 0x7b, 0x9c, 0xf9, 0x23, 0xdc, 0x50, 0x6e, 0xb9,
0xb8, 0xf2, 0x2c, 0x5d, 0xf7, 0x4f, 0x31, 0xb6, 0xf6, 0x4f, 0xc7, 0xfe, 0x41, 0x55,
0x63, 0xdd, 0x9f, 0x89, 0x09, 0x90, 0x6c, 0xff, 0xee, 0xae, 0xcb, 0xba, 0x4d, 0x17,
0x30, 0xc6, 0x18, 0xf3, 0x67, 0x5e, 0xc1, 0xed, 0x21, 0x5d, 0x00, 0x0a, 0x00, 0x00,
}),
},
}
testRepositories = map[string]map[string]string{
"foo42/bar": {
"latest": "42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d",
"test": "42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d",
},
}
mockHosts = map[string][]net.IP{
"": {net.ParseIP("0.0.0.0")},
"localhost": {net.ParseIP("127.0.0.1"), net.ParseIP("::1")},
"example.com": {net.ParseIP("42.42.42.42")},
"other.com": {net.ParseIP("43.43.43.43")},
}
)
func init() {
@ -97,14 +25,6 @@ func init() {
// /v1/
r.HandleFunc("/v1/_ping", handlerGetPing).Methods(http.MethodGet)
r.HandleFunc("/v1/images/{image_id:[^/]+}/{action:json|layer|ancestry}", handlerGetImage).Methods(http.MethodGet)
r.HandleFunc("/v1/images/{image_id:[^/]+}/{action:json|layer|checksum}", handlerPutImage).Methods(http.MethodPut)
r.HandleFunc("/v1/repositories/{repository:.+}/tags", handlerGetDeleteTags).Methods(http.MethodGet, http.MethodDelete)
r.HandleFunc("/v1/repositories/{repository:.+}/tags/{tag:.+}", handlerGetTag).Methods(http.MethodGet)
r.HandleFunc("/v1/repositories/{repository:.+}/tags/{tag:.+}", handlerPutTag).Methods(http.MethodPut)
r.HandleFunc("/v1/users{null:.*}", handlerUsers).Methods(http.MethodGet, http.MethodPost, http.MethodPut)
r.HandleFunc("/v1/repositories/{repository:.+}{action:/images|/}", handlerImages).Methods(http.MethodGet, http.MethodPut, http.MethodDelete)
r.HandleFunc("/v1/repositories/{repository:.+}/auth", handlerAuth).Methods(http.MethodPut)
r.HandleFunc("/v1/search", handlerSearch).Methods(http.MethodGet)
// /v2/
@ -119,6 +39,12 @@ func init() {
// I believe in future Go versions this will fail, so let's fix it later
return net.LookupIP(host)
}
mockHosts := map[string][]net.IP{
"": {net.ParseIP("0.0.0.0")},
"localhost": {net.ParseIP("127.0.0.1"), net.ParseIP("::1")},
"example.com": {net.ParseIP("42.42.42.42")},
"other.com": {net.ParseIP("43.43.43.43")},
}
for h, addrs := range mockHosts {
if host == h {
return addrs, nil
@ -135,7 +61,7 @@ func init() {
func handlerAccessLog(handler http.Handler) http.Handler {
logHandler := func(w http.ResponseWriter, r *http.Request) {
logrus.Debugf("%s \"%s %s\"", r.RemoteAddr, r.Method, r.URL)
logrus.Debugf(`%s "%s %s"`, r.RemoteAddr, r.Method, r.URL)
handler.ServeHTTP(w, r)
}
return http.HandlerFunc(logHandler)
@ -149,22 +75,22 @@ func makeHTTPSURL(req string) string {
return testHTTPSServer.URL + req
}
func makeIndex(req string) *registrytypes.IndexInfo {
index := &registrytypes.IndexInfo{
func makeIndex(req string) *registry.IndexInfo {
index := &registry.IndexInfo{
Name: makeURL(req),
}
return index
}
func makeHTTPSIndex(req string) *registrytypes.IndexInfo {
index := &registrytypes.IndexInfo{
func makeHTTPSIndex(req string) *registry.IndexInfo {
index := &registry.IndexInfo{
Name: makeHTTPSURL(req),
}
return index
}
func makePublicIndex() *registrytypes.IndexInfo {
index := &registrytypes.IndexInfo{
func makePublicIndex() *registry.IndexInfo {
index := &registry.IndexInfo{
Name: IndexServer,
Secure: true,
Official: true,
@ -203,252 +129,15 @@ func writeResponse(w http.ResponseWriter, message interface{}, code int) {
w.Write(body)
}
func readJSON(r *http.Request, dest interface{}) error {
body, err := io.ReadAll(r.Body)
if err != nil {
return err
}
return json.Unmarshal(body, dest)
}
func apiError(w http.ResponseWriter, message string, code int) {
body := map[string]string{
"error": message,
}
writeResponse(w, body, code)
}
func assertEqual(t *testing.T, a interface{}, b interface{}, message string) {
if a == b {
return
}
if len(message) == 0 {
message = fmt.Sprintf("%v != %v", a, b)
}
t.Fatal(message)
}
func assertNotEqual(t *testing.T, a interface{}, b interface{}, message string) {
if a != b {
return
}
if len(message) == 0 {
message = fmt.Sprintf("%v == %v", a, b)
}
t.Fatal(message)
}
// Similar to assertEqual, but does not stop test
func checkEqual(t *testing.T, a interface{}, b interface{}, messagePrefix string) {
if a == b {
return
}
message := fmt.Sprintf("%v != %v", a, b)
if len(messagePrefix) != 0 {
message = messagePrefix + ": " + message
}
t.Error(message)
}
// Similar to assertNotEqual, but does not stop test
func checkNotEqual(t *testing.T, a interface{}, b interface{}, messagePrefix string) {
if a != b {
return
}
message := fmt.Sprintf("%v == %v", a, b)
if len(messagePrefix) != 0 {
message = messagePrefix + ": " + message
}
t.Error(message)
}
func requiresAuth(w http.ResponseWriter, r *http.Request) bool {
writeCookie := func() {
value := fmt.Sprintf("FAKE-SESSION-%d", time.Now().UnixNano())
cookie := &http.Cookie{Name: "session", Value: value, MaxAge: 3600}
http.SetCookie(w, cookie)
// FIXME(sam): this should be sent only on Index routes
value = fmt.Sprintf("FAKE-TOKEN-%d", time.Now().UnixNano())
w.Header().Add("X-Docker-Token", value)
}
if len(r.Cookies()) > 0 {
writeCookie()
return true
}
if len(r.Header.Get("Authorization")) > 0 {
writeCookie()
return true
}
w.Header().Add("WWW-Authenticate", "token")
apiError(w, "Wrong auth", http.StatusUnauthorized)
return false
}
func handlerGetPing(w http.ResponseWriter, r *http.Request) {
writeResponse(w, true, http.StatusOK)
}
func handlerGetImage(w http.ResponseWriter, r *http.Request) {
if !requiresAuth(w, r) {
return
}
vars := mux.Vars(r)
layer, exists := testLayers[vars["image_id"]]
if !exists {
http.NotFound(w, r)
return
}
writeHeaders(w)
layerSize := len(layer["layer"])
w.Header().Add("X-Docker-Size", strconv.Itoa(layerSize))
io.WriteString(w, layer[vars["action"]])
}
func handlerPutImage(w http.ResponseWriter, r *http.Request) {
if !requiresAuth(w, r) {
return
}
vars := mux.Vars(r)
imageID := vars["image_id"]
action := vars["action"]
layer, exists := testLayers[imageID]
if !exists {
if action != "json" {
http.NotFound(w, r)
return
}
layer = make(map[string]string)
testLayers[imageID] = layer
}
if checksum := r.Header.Get("X-Docker-Checksum"); checksum != "" {
if checksum != layer["checksum_simple"] && checksum != layer["checksum_tarsum"] {
apiError(w, "Wrong checksum", http.StatusBadRequest)
return
}
}
body, err := io.ReadAll(r.Body)
if err != nil {
apiError(w, fmt.Sprintf("Error: %s", err), http.StatusInternalServerError)
return
}
layer[action] = string(body)
writeResponse(w, true, http.StatusOK)
}
func handlerGetDeleteTags(w http.ResponseWriter, r *http.Request) {
if !requiresAuth(w, r) {
return
}
repositoryName, err := reference.WithName(mux.Vars(r)["repository"])
if err != nil {
apiError(w, "Could not parse repository", http.StatusBadRequest)
return
}
tags, exists := testRepositories[repositoryName.String()]
if !exists {
apiError(w, "Repository not found", http.StatusNotFound)
return
}
if r.Method == http.MethodDelete {
delete(testRepositories, repositoryName.String())
writeResponse(w, true, http.StatusOK)
return
}
writeResponse(w, tags, http.StatusOK)
}
func handlerGetTag(w http.ResponseWriter, r *http.Request) {
if !requiresAuth(w, r) {
return
}
vars := mux.Vars(r)
repositoryName, err := reference.WithName(vars["repository"])
if err != nil {
apiError(w, "Could not parse repository", http.StatusBadRequest)
return
}
tagName := vars["tag"]
tags, exists := testRepositories[repositoryName.String()]
if !exists {
apiError(w, "Repository not found", http.StatusNotFound)
return
}
tag, exists := tags[tagName]
if !exists {
apiError(w, "Tag not found", http.StatusNotFound)
return
}
writeResponse(w, tag, http.StatusOK)
}
func handlerPutTag(w http.ResponseWriter, r *http.Request) {
if !requiresAuth(w, r) {
return
}
vars := mux.Vars(r)
repositoryName, err := reference.WithName(vars["repository"])
if err != nil {
apiError(w, "Could not parse repository", http.StatusBadRequest)
return
}
tagName := vars["tag"]
tags, exists := testRepositories[repositoryName.String()]
if !exists {
tags = make(map[string]string)
testRepositories[repositoryName.String()] = tags
}
tagValue := ""
readJSON(r, tagValue)
tags[tagName] = tagValue
writeResponse(w, true, http.StatusOK)
}
func handlerUsers(w http.ResponseWriter, r *http.Request) {
code := http.StatusOK
if r.Method == http.MethodPost {
code = http.StatusCreated
} else if r.Method == http.MethodPut {
code = http.StatusNoContent
}
writeResponse(w, "", code)
}
func handlerImages(w http.ResponseWriter, r *http.Request) {
u, _ := url.Parse(testHTTPServer.URL)
w.Header().Add("X-Docker-Endpoints", fmt.Sprintf("%s , %s ", u.Host, "test.example.com"))
w.Header().Add("X-Docker-Token", fmt.Sprintf("FAKE-SESSION-%d", time.Now().UnixNano()))
if r.Method == http.MethodPut {
if strings.HasSuffix(r.URL.Path, "images") {
writeResponse(w, "", http.StatusNoContent)
return
}
writeResponse(w, "", http.StatusOK)
return
}
if r.Method == http.MethodDelete {
writeResponse(w, "", http.StatusNoContent)
return
}
var images []map[string]string
for imageID, layer := range testLayers {
image := make(map[string]string)
image["id"] = imageID
image["checksum"] = layer["checksum_tarsum"]
image["Tag"] = "latest"
images = append(images, image)
}
writeResponse(w, images, http.StatusOK)
}
func handlerAuth(w http.ResponseWriter, r *http.Request) {
writeResponse(w, "OK", http.StatusOK)
}
func handlerSearch(w http.ResponseWriter, r *http.Request) {
result := &registrytypes.SearchResults{
result := &registry.SearchResults{
Query: "fakequery",
NumResults: 1,
Results: []registrytypes.SearchResult{{Name: "fakeimage", StarCount: 42}},
Results: []registry.SearchResult{{Name: "fakeimage", StarCount: 42}},
}
writeResponse(w, result, http.StatusOK)
}
@ -458,18 +147,6 @@ func TestPing(t *testing.T) {
if err != nil {
t.Fatal(err)
}
assertEqual(t, res.StatusCode, http.StatusOK, "")
assertEqual(t, res.Header.Get("X-Docker-Registry-Config"), "mock",
"This is not a Mocked Registry")
assert.Equal(t, res.StatusCode, http.StatusOK, "")
assert.Equal(t, res.Header.Get("X-Docker-Registry-Config"), "mock", "This is not a Mocked Registry")
}
/* Uncomment this to test Mocked Registry locally with curl
* WARNING: Don't push on the repos uncommented, it'll block the tests
*
func TestWait(t *testing.T) {
logrus.Println("Test HTTP server ready and waiting:", testHTTPServer.URL)
c := make(chan int)
<-c
}
//*/

View file

@ -10,25 +10,28 @@ import (
"github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/client/transport"
"github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/registry"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/skip"
)
func spawnTestRegistrySession(t *testing.T) *Session {
func spawnTestRegistrySession(t *testing.T) *session {
authConfig := &types.AuthConfig{}
endpoint, err := NewV1Endpoint(makeIndex("/v1/"), "", nil)
endpoint, err := newV1Endpoint(makeIndex("/v1/"), "", nil)
if err != nil {
t.Fatal(err)
}
userAgent := "docker test client"
var tr http.RoundTripper = debugTransport{NewTransport(nil), t.Log}
var tr http.RoundTripper = debugTransport{newTransport(nil), t.Log}
tr = transport.NewTransport(AuthTransport(tr, authConfig, false), Headers(userAgent, nil)...)
client := HTTPClient(tr)
r, err := NewSession(client, authConfig, endpoint)
if err != nil {
client := httpClient(tr)
if err := authorizeClient(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.
@ -45,17 +48,17 @@ func spawnTestRegistrySession(t *testing.T) *Session {
func TestPingRegistryEndpoint(t *testing.T) {
skip.If(t, os.Getuid() != 0, "skipping test that requires root")
testPing := func(index *registrytypes.IndexInfo, expectedStandalone bool, assertMessage string) {
ep, err := NewV1Endpoint(index, "", nil)
testPing := func(index *registry.IndexInfo, expectedStandalone bool, assertMessage string) {
ep, err := newV1Endpoint(index, "", nil)
if err != nil {
t.Fatal(err)
}
regInfo, err := ep.Ping()
regInfo, err := ep.ping()
if err != nil {
t.Fatal(err)
}
assertEqual(t, regInfo.Standalone, expectedStandalone, assertMessage)
assert.Equal(t, regInfo.Standalone, expectedStandalone, assertMessage)
}
testPing(makeIndex("/v1/"), true, "Expected standalone to be true (default)")
@ -66,61 +69,59 @@ func TestPingRegistryEndpoint(t *testing.T) {
func TestEndpoint(t *testing.T) {
skip.If(t, os.Getuid() != 0, "skipping test that requires root")
// Simple wrapper to fail test if err != nil
expandEndpoint := func(index *registrytypes.IndexInfo) *V1Endpoint {
endpoint, err := NewV1Endpoint(index, "", nil)
expandEndpoint := func(index *registry.IndexInfo) *v1Endpoint {
endpoint, err := newV1Endpoint(index, "", nil)
if err != nil {
t.Fatal(err)
}
return endpoint
}
assertInsecureIndex := func(index *registrytypes.IndexInfo) {
assertInsecureIndex := func(index *registry.IndexInfo) {
index.Secure = true
_, err := NewV1Endpoint(index, "", nil)
assertNotEqual(t, err, nil, index.Name+": Expected error for insecure index")
assertEqual(t, strings.Contains(err.Error(), "insecure-registry"), true, index.Name+": Expected insecure-registry error for insecure index")
_, err := newV1Endpoint(index, "", nil)
assert.ErrorContains(t, err, "insecure-registry", index.Name+": Expected insecure-registry error for insecure index")
index.Secure = false
}
assertSecureIndex := func(index *registrytypes.IndexInfo) {
assertSecureIndex := func(index *registry.IndexInfo) {
index.Secure = true
_, err := NewV1Endpoint(index, "", nil)
assertNotEqual(t, err, nil, index.Name+": Expected cert error for secure index")
assertEqual(t, strings.Contains(err.Error(), "certificate signed by unknown authority"), true, index.Name+": Expected cert error for secure index")
_, err := newV1Endpoint(index, "", nil)
assert.ErrorContains(t, err, "certificate signed by unknown authority", index.Name+": Expected cert error for secure index")
index.Secure = false
}
index := &registrytypes.IndexInfo{}
index := &registry.IndexInfo{}
index.Name = makeURL("/v1/")
endpoint := expandEndpoint(index)
assertEqual(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name)
assert.Equal(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name)
assertInsecureIndex(index)
index.Name = makeURL("")
endpoint = expandEndpoint(index)
assertEqual(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/")
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)
assertEqual(t, endpoint.String(), httpURL+"/v1/", index.Name+": Expected endpoint to be "+httpURL+"/v1/")
assert.Equal(t, endpoint.String(), httpURL+"/v1/", index.Name+": Expected endpoint to be "+httpURL+"/v1/")
assertInsecureIndex(index)
index.Name = makeHTTPSURL("/v1/")
endpoint = expandEndpoint(index)
assertEqual(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name)
assert.Equal(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name)
assertSecureIndex(index)
index.Name = makeHTTPSURL("")
endpoint = expandEndpoint(index)
assertEqual(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/")
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)
assertEqual(t, endpoint.String(), httpsURL+"/v1/", index.Name+": Expected endpoint to be "+httpsURL+"/v1/")
assert.Equal(t, endpoint.String(), httpsURL+"/v1/", index.Name+": Expected endpoint to be "+httpsURL+"/v1/")
assertSecureIndex(index)
badEndpoints := []string{
@ -132,14 +133,14 @@ func TestEndpoint(t *testing.T) {
}
for _, address := range badEndpoints {
index.Name = address
_, err := NewV1Endpoint(index, "", nil)
checkNotEqual(t, err, nil, "Expected error while expanding bad endpoint")
_, err := newV1Endpoint(index, "", nil)
assert.Check(t, err != nil, "Expected error while expanding bad endpoint: %s", address)
}
}
func TestParseRepositoryInfo(t *testing.T) {
type staticRepositoryInfo struct {
Index *registrytypes.IndexInfo
Index *registry.IndexInfo
RemoteName string
CanonicalName string
LocalName string
@ -148,7 +149,7 @@ func TestParseRepositoryInfo(t *testing.T) {
expectedRepoInfos := map[string]staticRepositoryInfo{
"fooo/bar": {
Index: &registrytypes.IndexInfo{
Index: &registry.IndexInfo{
Name: IndexName,
Official: true,
},
@ -158,7 +159,7 @@ func TestParseRepositoryInfo(t *testing.T) {
Official: false,
},
"library/ubuntu": {
Index: &registrytypes.IndexInfo{
Index: &registry.IndexInfo{
Name: IndexName,
Official: true,
},
@ -168,7 +169,7 @@ func TestParseRepositoryInfo(t *testing.T) {
Official: true,
},
"nonlibrary/ubuntu": {
Index: &registrytypes.IndexInfo{
Index: &registry.IndexInfo{
Name: IndexName,
Official: true,
},
@ -178,7 +179,7 @@ func TestParseRepositoryInfo(t *testing.T) {
Official: false,
},
"ubuntu": {
Index: &registrytypes.IndexInfo{
Index: &registry.IndexInfo{
Name: IndexName,
Official: true,
},
@ -188,7 +189,7 @@ func TestParseRepositoryInfo(t *testing.T) {
Official: true,
},
"other/library": {
Index: &registrytypes.IndexInfo{
Index: &registry.IndexInfo{
Name: IndexName,
Official: true,
},
@ -198,7 +199,7 @@ func TestParseRepositoryInfo(t *testing.T) {
Official: false,
},
"127.0.0.1:8000/private/moonbase": {
Index: &registrytypes.IndexInfo{
Index: &registry.IndexInfo{
Name: "127.0.0.1:8000",
Official: false,
},
@ -208,7 +209,7 @@ func TestParseRepositoryInfo(t *testing.T) {
Official: false,
},
"127.0.0.1:8000/privatebase": {
Index: &registrytypes.IndexInfo{
Index: &registry.IndexInfo{
Name: "127.0.0.1:8000",
Official: false,
},
@ -218,7 +219,7 @@ func TestParseRepositoryInfo(t *testing.T) {
Official: false,
},
"localhost:8000/private/moonbase": {
Index: &registrytypes.IndexInfo{
Index: &registry.IndexInfo{
Name: "localhost:8000",
Official: false,
},
@ -228,7 +229,7 @@ func TestParseRepositoryInfo(t *testing.T) {
Official: false,
},
"localhost:8000/privatebase": {
Index: &registrytypes.IndexInfo{
Index: &registry.IndexInfo{
Name: "localhost:8000",
Official: false,
},
@ -238,7 +239,7 @@ func TestParseRepositoryInfo(t *testing.T) {
Official: false,
},
"example.com/private/moonbase": {
Index: &registrytypes.IndexInfo{
Index: &registry.IndexInfo{
Name: "example.com",
Official: false,
},
@ -248,7 +249,7 @@ func TestParseRepositoryInfo(t *testing.T) {
Official: false,
},
"example.com/privatebase": {
Index: &registrytypes.IndexInfo{
Index: &registry.IndexInfo{
Name: "example.com",
Official: false,
},
@ -258,7 +259,7 @@ func TestParseRepositoryInfo(t *testing.T) {
Official: false,
},
"example.com:8000/private/moonbase": {
Index: &registrytypes.IndexInfo{
Index: &registry.IndexInfo{
Name: "example.com:8000",
Official: false,
},
@ -268,7 +269,7 @@ func TestParseRepositoryInfo(t *testing.T) {
Official: false,
},
"example.com:8000/privatebase": {
Index: &registrytypes.IndexInfo{
Index: &registry.IndexInfo{
Name: "example.com:8000",
Official: false,
},
@ -278,7 +279,7 @@ func TestParseRepositoryInfo(t *testing.T) {
Official: false,
},
"localhost/private/moonbase": {
Index: &registrytypes.IndexInfo{
Index: &registry.IndexInfo{
Name: "localhost",
Official: false,
},
@ -288,7 +289,7 @@ func TestParseRepositoryInfo(t *testing.T) {
Official: false,
},
"localhost/privatebase": {
Index: &registrytypes.IndexInfo{
Index: &registry.IndexInfo{
Name: "localhost",
Official: false,
},
@ -298,7 +299,7 @@ func TestParseRepositoryInfo(t *testing.T) {
Official: false,
},
IndexName + "/public/moonbase": {
Index: &registrytypes.IndexInfo{
Index: &registry.IndexInfo{
Name: IndexName,
Official: true,
},
@ -308,7 +309,7 @@ func TestParseRepositoryInfo(t *testing.T) {
Official: false,
},
"index." + IndexName + "/public/moonbase": {
Index: &registrytypes.IndexInfo{
Index: &registry.IndexInfo{
Name: IndexName,
Official: true,
},
@ -318,7 +319,7 @@ func TestParseRepositoryInfo(t *testing.T) {
Official: false,
},
"ubuntu-12.04-base": {
Index: &registrytypes.IndexInfo{
Index: &registry.IndexInfo{
Name: IndexName,
Official: true,
},
@ -328,7 +329,7 @@ func TestParseRepositoryInfo(t *testing.T) {
Official: true,
},
IndexName + "/ubuntu-12.04-base": {
Index: &registrytypes.IndexInfo{
Index: &registry.IndexInfo{
Name: IndexName,
Official: true,
},
@ -338,7 +339,7 @@ func TestParseRepositoryInfo(t *testing.T) {
Official: true,
},
"index." + IndexName + "/ubuntu-12.04-base": {
Index: &registrytypes.IndexInfo{
Index: &registry.IndexInfo{
Name: IndexName,
Official: true,
},
@ -359,34 +360,34 @@ func TestParseRepositoryInfo(t *testing.T) {
if err != nil {
t.Error(err)
} else {
checkEqual(t, repoInfo.Index.Name, expectedRepoInfo.Index.Name, reposName)
checkEqual(t, reference.Path(repoInfo.Name), expectedRepoInfo.RemoteName, reposName)
checkEqual(t, reference.FamiliarName(repoInfo.Name), expectedRepoInfo.LocalName, reposName)
checkEqual(t, repoInfo.Name.Name(), expectedRepoInfo.CanonicalName, reposName)
checkEqual(t, repoInfo.Index.Official, expectedRepoInfo.Index.Official, reposName)
checkEqual(t, repoInfo.Official, expectedRepoInfo.Official, reposName)
assert.Check(t, is.Equal(repoInfo.Index.Name, expectedRepoInfo.Index.Name), reposName)
assert.Check(t, is.Equal(reference.Path(repoInfo.Name), expectedRepoInfo.RemoteName), reposName)
assert.Check(t, is.Equal(reference.FamiliarName(repoInfo.Name), expectedRepoInfo.LocalName), reposName)
assert.Check(t, is.Equal(repoInfo.Name.Name(), expectedRepoInfo.CanonicalName), reposName)
assert.Check(t, is.Equal(repoInfo.Index.Official, expectedRepoInfo.Index.Official), reposName)
assert.Check(t, is.Equal(repoInfo.Official, expectedRepoInfo.Official), reposName)
}
}
}
func TestNewIndexInfo(t *testing.T) {
testIndexInfo := func(config *serviceConfig, expectedIndexInfos map[string]*registrytypes.IndexInfo) {
testIndexInfo := func(config *serviceConfig, expectedIndexInfos map[string]*registry.IndexInfo) {
for indexName, expectedIndexInfo := range expectedIndexInfos {
index, err := newIndexInfo(config, indexName)
if err != nil {
t.Fatal(err)
} else {
checkEqual(t, index.Name, expectedIndexInfo.Name, indexName+" name")
checkEqual(t, index.Official, expectedIndexInfo.Official, indexName+" is official")
checkEqual(t, index.Secure, expectedIndexInfo.Secure, indexName+" is secure")
checkEqual(t, len(index.Mirrors), len(expectedIndexInfo.Mirrors), indexName+" mirrors")
assert.Check(t, is.Equal(index.Name, expectedIndexInfo.Name), indexName+" name")
assert.Check(t, is.Equal(index.Official, expectedIndexInfo.Official), indexName+" is official")
assert.Check(t, is.Equal(index.Secure, expectedIndexInfo.Secure), indexName+" is secure")
assert.Check(t, is.Equal(len(index.Mirrors), len(expectedIndexInfo.Mirrors)), indexName+" mirrors")
}
}
}
config := emptyServiceConfig
var noMirrors []string
expectedIndexInfos := map[string]*registrytypes.IndexInfo{
expectedIndexInfos := map[string]*registry.IndexInfo{
IndexName: {
Name: IndexName,
Official: true,
@ -421,7 +422,7 @@ func TestNewIndexInfo(t *testing.T) {
t.Fatal(err)
}
expectedIndexInfos = map[string]*registrytypes.IndexInfo{
expectedIndexInfos = map[string]*registry.IndexInfo{
IndexName: {
Name: IndexName,
Official: true,
@ -471,7 +472,7 @@ func TestNewIndexInfo(t *testing.T) {
if err != nil {
t.Fatal(err)
}
expectedIndexInfos = map[string]*registrytypes.IndexInfo{
expectedIndexInfos = map[string]*registry.IndexInfo{
"example.com": {
Name: "example.com",
Official: false,
@ -520,7 +521,7 @@ func TestMirrorEndpointLookup(t *testing.T) {
if err != nil {
t.Fatal(err)
}
s := DefaultService{config: cfg}
s := defaultService{config: cfg}
imageName, err := reference.WithName(IndexName + "/test/image")
if err != nil {
@ -545,16 +546,16 @@ func TestMirrorEndpointLookup(t *testing.T) {
func TestSearchRepositories(t *testing.T) {
r := spawnTestRegistrySession(t)
results, err := r.SearchRepositories("fakequery", 25)
results, err := r.searchRepositories("fakequery", 25)
if err != nil {
t.Fatal(err)
}
if results == nil {
t.Fatal("Expected non-nil SearchResults object")
}
assertEqual(t, results.NumResults, 1, "Expected 1 search results")
assertEqual(t, results.Query, "fakequery", "Expected 'fakequery' as query")
assertEqual(t, results.Results[0].StarCount, 42, "Expected 'fakeimage' to have 42 stars")
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 TestTrustedLocation(t *testing.T) {
@ -580,7 +581,7 @@ func TestAddRequiredHeadersToRedirectedRequests(t *testing.T) {
reqFrom.Header.Add("Authorization", "super_secret")
reqTo, _ := http.NewRequest(http.MethodGet, urls[1], nil)
addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom})
_ = addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom})
if len(reqTo.Header) != 1 {
t.Fatalf("Expected 1 headers, got %d", len(reqTo.Header))
@ -604,7 +605,7 @@ func TestAddRequiredHeadersToRedirectedRequests(t *testing.T) {
reqFrom.Header.Add("Authorization", "super_secret")
reqTo, _ := http.NewRequest(http.MethodGet, urls[1], nil)
addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom})
_ = addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom})
if len(reqTo.Header) != 2 {
t.Fatalf("Expected 2 headers, got %d", len(reqTo.Header))
@ -659,7 +660,7 @@ func TestAllowNondistributableArtifacts(t *testing.T) {
if err != nil {
t.Error(err)
}
if v := allowNondistributableArtifacts(config, tt.addr); v != tt.expected {
if v := config.allowNondistributableArtifacts(tt.addr); v != tt.expected {
t.Errorf("allowNondistributableArtifacts failed for %q %v, expected %v got %v", tt.addr, tt.registries, tt.expected, v)
}
}
@ -702,7 +703,7 @@ func TestIsSecureIndex(t *testing.T) {
if err != nil {
t.Error(err)
}
if sec := isSecureIndex(config, tt.addr); sec != tt.expected {
if sec := config.isSecureIndex(tt.addr); sec != tt.expected {
t.Errorf("isSecureIndex failed for %q %v, expected %v got %v", tt.addr, tt.insecureRegistries, tt.expected, sec)
}
}

View file

@ -11,9 +11,8 @@ import (
"github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/client/auth"
"github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/errdefs"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
@ -28,85 +27,64 @@ type Service interface {
LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error)
LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error)
ResolveRepository(name reference.Named) (*RepositoryInfo, error)
Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error)
ServiceConfig() *registrytypes.ServiceConfig
Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registry.SearchResults, error)
ServiceConfig() *registry.ServiceConfig
TLSConfig(hostname string) (*tls.Config, error)
LoadAllowNondistributableArtifacts([]string) error
LoadMirrors([]string) error
LoadInsecureRegistries([]string) error
}
// DefaultService is a registry service. It tracks configuration data such as a list
// defaultService is a registry service. It tracks configuration data such as a list
// of mirrors.
type DefaultService struct {
type defaultService struct {
config *serviceConfig
mu sync.Mutex
mu sync.RWMutex
}
// NewService returns a new instance of DefaultService ready to be
// NewService returns a new instance of defaultService ready to be
// installed into an engine.
func NewService(options ServiceOptions) (*DefaultService, error) {
func NewService(options ServiceOptions) (Service, error) {
config, err := newServiceConfig(options)
return &DefaultService{config: config}, err
return &defaultService{config: config}, err
}
// ServiceConfig returns the public registry service configuration.
func (s *DefaultService) ServiceConfig() *registrytypes.ServiceConfig {
s.mu.Lock()
defer s.mu.Unlock()
servConfig := registrytypes.ServiceConfig{
AllowNondistributableArtifactsCIDRs: make([]*(registrytypes.NetIPNet), 0),
AllowNondistributableArtifactsHostnames: make([]string, 0),
InsecureRegistryCIDRs: make([]*(registrytypes.NetIPNet), 0),
IndexConfigs: make(map[string]*(registrytypes.IndexInfo)),
Mirrors: make([]string, 0),
}
// construct a new ServiceConfig which will not retrieve s.Config directly,
// and look up items in s.config with mu locked
servConfig.AllowNondistributableArtifactsCIDRs = append(servConfig.AllowNondistributableArtifactsCIDRs, s.config.ServiceConfig.AllowNondistributableArtifactsCIDRs...)
servConfig.AllowNondistributableArtifactsHostnames = append(servConfig.AllowNondistributableArtifactsHostnames, s.config.ServiceConfig.AllowNondistributableArtifactsHostnames...)
servConfig.InsecureRegistryCIDRs = append(servConfig.InsecureRegistryCIDRs, s.config.ServiceConfig.InsecureRegistryCIDRs...)
for key, value := range s.config.ServiceConfig.IndexConfigs {
servConfig.IndexConfigs[key] = value
}
servConfig.Mirrors = append(servConfig.Mirrors, s.config.ServiceConfig.Mirrors...)
return &servConfig
// ServiceConfig returns a copy of the public registry service's configuration.
func (s *defaultService) ServiceConfig() *registry.ServiceConfig {
s.mu.RLock()
defer s.mu.RUnlock()
return s.config.copy()
}
// LoadAllowNondistributableArtifacts loads allow-nondistributable-artifacts registries for Service.
func (s *DefaultService) LoadAllowNondistributableArtifacts(registries []string) error {
func (s *defaultService) LoadAllowNondistributableArtifacts(registries []string) error {
s.mu.Lock()
defer s.mu.Unlock()
return s.config.LoadAllowNondistributableArtifacts(registries)
return s.config.loadAllowNondistributableArtifacts(registries)
}
// LoadMirrors loads registry mirrors for Service
func (s *DefaultService) LoadMirrors(mirrors []string) error {
func (s *defaultService) LoadMirrors(mirrors []string) error {
s.mu.Lock()
defer s.mu.Unlock()
return s.config.LoadMirrors(mirrors)
return s.config.loadMirrors(mirrors)
}
// LoadInsecureRegistries loads insecure registries for Service
func (s *DefaultService) LoadInsecureRegistries(registries []string) error {
func (s *defaultService) LoadInsecureRegistries(registries []string) error {
s.mu.Lock()
defer s.mu.Unlock()
return s.config.LoadInsecureRegistries(registries)
return s.config.loadInsecureRegistries(registries)
}
// Auth contacts the public registry with the provided credentials,
// and returns OK if authentication was successful.
// It can be used to verify the validity of a client's credentials.
func (s *DefaultService) Auth(ctx context.Context, authConfig *types.AuthConfig, userAgent string) (status, token string, err error) {
func (s *defaultService) Auth(ctx context.Context, authConfig *types.AuthConfig, userAgent string) (status, token string, err error) {
// TODO Use ctx when searching for repositories
var registryHostName = IndexHostname
@ -117,7 +95,7 @@ func (s *DefaultService) Auth(ctx context.Context, authConfig *types.AuthConfig,
}
u, err := url.Parse(serverAddress)
if err != nil {
return "", "", errdefs.InvalidParameter(errors.Errorf("unable to parse server address: %v", err))
return "", "", invalidParamWrapf(err, "unable to parse server address")
}
registryHostName = u.Host
}
@ -127,7 +105,7 @@ func (s *DefaultService) Auth(ctx context.Context, authConfig *types.AuthConfig,
// to a mirror.
endpoints, err := s.LookupPushEndpoints(registryHostName)
if err != nil {
return "", "", errdefs.InvalidParameter(err)
return "", "", invalidParam(err)
}
for _, endpoint := range endpoints {
@ -159,25 +137,28 @@ func splitReposSearchTerm(reposName string) (string, string) {
// Search queries the public registry for images matching the specified
// search terms, and returns the results.
func (s *DefaultService) Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) {
func (s *defaultService) Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registry.SearchResults, error) {
// TODO Use ctx when searching for repositories
if err := validateNoScheme(term); err != nil {
return nil, err
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.Lock()
s.mu.RLock()
index, err := newIndexInfo(s.config, indexName)
s.mu.Unlock()
s.mu.RUnlock()
if err != nil {
return nil, err
}
if index.Official {
// If pull "library/foo", it's stored locally under "foo"
remoteName = strings.TrimPrefix(remoteName, "library/")
}
// *TODO: Search multiple indexes.
endpoint, err := NewV1Endpoint(index, userAgent, headers)
endpoint, err := newV1Endpoint(index, userAgent, headers)
if err != nil {
return nil, err
}
@ -196,7 +177,7 @@ func (s *DefaultService) Search(ctx context.Context, term string, limit int, aut
v2Client, err := v2AuthHTTPClient(endpoint.URL, endpoint.client.Transport, modifiers, creds, scopes)
if err != nil {
if fErr, ok := err.(fallbackError); ok {
logrus.Errorf("Cannot use identity token for search, v2 auth not supported: %v", fErr.err)
logrus.WithError(fErr.err).Error("cannot use identity token for search, v2 auth not supported")
} else {
return nil, err
}
@ -218,20 +199,14 @@ func (s *DefaultService) Search(ctx context.Context, term string, limit int, aut
}
}
r := newSession(client, authConfig, endpoint)
if index.Official {
// If pull "library/foo", it's stored locally under "foo"
remoteName = strings.TrimPrefix(remoteName, "library/")
}
return r.SearchRepositories(remoteName, limit)
return newSession(client, endpoint).searchRepositories(remoteName, limit)
}
// ResolveRepository splits a repository name into its components
// and configuration of the associated registry.
func (s *DefaultService) ResolveRepository(name reference.Named) (*RepositoryInfo, error) {
s.mu.Lock()
defer s.mu.Unlock()
func (s *defaultService) ResolveRepository(name reference.Named) (*RepositoryInfo, error) {
s.mu.RLock()
defer s.mu.RUnlock()
return newRepositoryInfo(s.config, name)
}
@ -247,32 +222,28 @@ type APIEndpoint struct {
}
// TLSConfig constructs a client TLS configuration based on server defaults
func (s *DefaultService) TLSConfig(hostname string) (*tls.Config, error) {
s.mu.Lock()
defer s.mu.Unlock()
func (s *defaultService) TLSConfig(hostname string) (*tls.Config, error) {
s.mu.RLock()
secure := s.config.isSecureIndex(hostname)
s.mu.RUnlock()
return s.tlsConfig(hostname)
}
// tlsConfig constructs a client TLS configuration based on server defaults
func (s *DefaultService) tlsConfig(hostname string) (*tls.Config, error) {
return newTLSConfig(hostname, isSecureIndex(s.config, hostname))
return newTLSConfig(hostname, secure)
}
// LookupPullEndpoints creates a list of v2 endpoints to try to pull from, in order of preference.
// It gives preference to mirrors over the actual registry, and HTTPS over plain HTTP.
func (s *DefaultService) LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
s.mu.Lock()
defer s.mu.Unlock()
func (s *defaultService) LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.lookupV2Endpoints(hostname)
}
// LookupPushEndpoints creates a list of v2 endpoints to try to push to, in order of preference.
// It gives preference to HTTPS over plain HTTP. Mirrors are not included.
func (s *DefaultService) LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
s.mu.Lock()
defer s.mu.Unlock()
func (s *defaultService) LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
s.mu.RLock()
defer s.mu.RUnlock()
allEndpoints, err := s.lookupV2Endpoints(hostname)
if err == nil {

View file

@ -7,8 +7,7 @@ import (
"github.com/docker/go-connections/tlsconfig"
)
func (s *DefaultService) lookupV2Endpoints(hostname string) (endpoints []APIEndpoint, err error) {
tlsConfig := tlsconfig.ServerDefault()
func (s *defaultService) lookupV2Endpoints(hostname string) (endpoints []APIEndpoint, err error) {
if hostname == DefaultNamespace || hostname == IndexHostname {
for _, mirror := range s.config.Mirrors {
if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") {
@ -16,9 +15,9 @@ func (s *DefaultService) lookupV2Endpoints(hostname string) (endpoints []APIEndp
}
mirrorURL, err := url.Parse(mirror)
if err != nil {
return nil, err
return nil, invalidParam(err)
}
mirrorTLSConfig, err := s.tlsConfig(mirrorURL.Host)
mirrorTLSConfig, err := newTLSConfig(mirrorURL.Host, s.config.isSecureIndex(mirrorURL.Host))
if err != nil {
return nil, err
}
@ -35,19 +34,18 @@ func (s *DefaultService) lookupV2Endpoints(hostname string) (endpoints []APIEndp
Version: APIVersion2,
Official: true,
TrimHostname: true,
TLSConfig: tlsConfig,
TLSConfig: tlsconfig.ServerDefault(),
})
return endpoints, nil
}
ana := allowNondistributableArtifacts(s.config, hostname)
tlsConfig, err = s.tlsConfig(hostname)
tlsConfig, err := newTLSConfig(hostname, s.config.isSecureIndex(hostname))
if err != nil {
return nil, err
}
ana := s.config.allowNondistributableArtifacts(hostname)
endpoints = []APIEndpoint{
{
URL: &url.URL{

View file

@ -12,7 +12,7 @@ import (
"sync"
"github.com/docker/docker/api/types"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/errdefs"
"github.com/docker/docker/pkg/ioutils"
"github.com/docker/docker/pkg/jsonmessage"
@ -21,13 +21,11 @@ import (
"github.com/sirupsen/logrus"
)
// A Session is used to communicate with a V1 registry
type Session struct {
indexEndpoint *V1Endpoint
// A session is used to communicate with a V1 registry
type session struct {
indexEndpoint *v1Endpoint
client *http.Client
// TODO(tiborvass): remove authConfig
authConfig *types.AuthConfig
id string
id string
}
type authTransport struct {
@ -149,13 +147,13 @@ func (tr *authTransport) CancelRequest(req *http.Request) {
}
}
func authorizeClient(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) error {
func authorizeClient(client *http.Client, authConfig *types.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()
info, err := endpoint.ping()
if err != nil {
return err
}
@ -171,43 +169,32 @@ func authorizeClient(client *http.Client, authConfig *types.AuthConfig, endpoint
jar, err := cookiejar.New(nil)
if err != nil {
return errors.New("cookiejar.New is not supposed to return an error")
return errdefs.System(errors.New("cookiejar.New is not supposed to return an error"))
}
client.Jar = jar
return nil
}
func newSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) *Session {
return &Session{
authConfig: authConfig,
func newSession(client *http.Client, endpoint *v1Endpoint) *session {
return &session{
client: client,
indexEndpoint: endpoint,
id: stringid.GenerateRandomID(),
}
}
// NewSession creates a new session
// TODO(tiborvass): remove authConfig param once registry client v2 is vendored
func NewSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) (*Session, error) {
if err := authorizeClient(client, authConfig, endpoint); err != nil {
return nil, err
}
return newSession(client, authConfig, endpoint), nil
}
// SearchRepositories performs a search against the remote repository
func (r *Session) SearchRepositories(term string, limit int) (*registrytypes.SearchResults, error) {
// searchRepositories performs a search against the remote repository
func (r *session) searchRepositories(term string, limit int) (*registry.SearchResults, error) {
if limit < 1 || limit > 100 {
return nil, errdefs.InvalidParameter(errors.Errorf("Limit %d is outside the range of [1, 100]", limit))
return nil, invalidParamf("limit %d is outside the range of [1, 100]", limit)
}
logrus.Debugf("Index server: %s", r.indexEndpoint)
u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term) + "&n=" + url.QueryEscape(fmt.Sprintf("%d", limit))
req, err := http.NewRequest(http.MethodGet, u, nil)
if err != nil {
return nil, errors.Wrap(errdefs.InvalidParameter(err), "Error building request")
return nil, invalidParamWrapf(err, "error building request")
}
// Have the AuthTransport send authentication, when logged in.
req.Header.Set("X-Docker-Token", "true")
@ -222,6 +209,6 @@ func (r *Session) SearchRepositories(term string, limit int) (*registrytypes.Sea
Code: res.StatusCode,
}
}
result := new(registrytypes.SearchResults)
result := new(registry.SearchResults)
return result, errors.Wrap(json.NewDecoder(res.Body).Decode(result), "error decoding registry search results")
}

View file

@ -2,39 +2,9 @@ package registry // import "github.com/docker/docker/registry"
import (
"github.com/docker/distribution/reference"
registrytypes "github.com/docker/docker/api/types/registry"
"github.com/docker/docker/api/types/registry"
)
// RepositoryData tracks the image list, list of endpoints for a repository
type RepositoryData struct {
// ImgList is a list of images in the repository
ImgList map[string]*ImgData
// Endpoints is a list of endpoints returned in X-Docker-Endpoints
Endpoints []string
}
// ImgData is used to transfer image checksums to and from the registry
type ImgData struct {
// ID is an opaque string that identifies the image
ID string `json:"id"`
Checksum string `json:"checksum,omitempty"`
ChecksumPayload string `json:"-"`
Tag string `json:",omitempty"`
}
// PingResult contains the information returned when pinging a registry. It
// indicates the registry's version and whether the registry claims to be a
// standalone registry.
type PingResult struct {
// Version is the registry version supplied by the registry in an HTTP
// header
Version string `json:"version"`
// 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"`
}
// APIVersion is an integral representation of an API version (presently
// either 1 or 2)
type APIVersion int
@ -58,7 +28,7 @@ var apiVersions = map[APIVersion]string{
type RepositoryInfo struct {
Name reference.Named
// Index points to registry information
Index *registrytypes.IndexInfo
Index *registry.IndexInfo
// Official indicates whether the repository is considered official.
// If the registry is official, and the normalized name does not
// contain a '/' (e.g. "foo"), then it is considered an official repo.