mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
9d6acbee92
PR #18590 caused compatibility issues with registries such as gcr.io which support both the v1 and v2 protocols, but do not provide the same set of images over both protocols. After #18590, pulls from these registries would never use the v1 protocol, because of the Docker-Distribution-Api-Version header indicating that v2 was supported. Fix the problem by making an exception for the case where a manifest is not found. This should allow fallback to v1 in case that image is exposed over the v1 protocol but not the v2 protocol. This avoids the overly aggressive fallback behavior before #18590 which would allow protocol fallback after almost any error, but restores interoperability with mixed v1/v2 registry setups. Fixes #18832 Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
255 lines
7.5 KiB
Go
255 lines
7.5 KiB
Go
// Package registry contains client primitives to interact with a remote Docker registry.
|
|
package registry
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/Sirupsen/logrus"
|
|
"github.com/docker/distribution/registry/api/errcode"
|
|
"github.com/docker/distribution/registry/api/v2"
|
|
"github.com/docker/distribution/registry/client"
|
|
"github.com/docker/distribution/registry/client/transport"
|
|
"github.com/docker/docker/dockerversion"
|
|
"github.com/docker/docker/pkg/parsers/kernel"
|
|
"github.com/docker/docker/pkg/tlsconfig"
|
|
"github.com/docker/docker/pkg/useragent"
|
|
)
|
|
|
|
var (
|
|
// ErrAlreadyExists is an error returned if an image being pushed
|
|
// already exists on the remote side
|
|
ErrAlreadyExists = errors.New("Image already exists")
|
|
errLoginRequired = errors.New("Authentication is required.")
|
|
)
|
|
|
|
// dockerUserAgent is the User-Agent the Docker client uses to identify itself.
|
|
// It is populated on init(), comprising version information of different components.
|
|
var dockerUserAgent string
|
|
|
|
func init() {
|
|
httpVersion := make([]useragent.VersionInfo, 0, 6)
|
|
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "docker", Version: dockerversion.Version})
|
|
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "go", Version: runtime.Version()})
|
|
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "git-commit", Version: dockerversion.GitCommit})
|
|
if kernelVersion, err := kernel.GetKernelVersion(); err == nil {
|
|
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "kernel", Version: kernelVersion.String()})
|
|
}
|
|
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "os", Version: runtime.GOOS})
|
|
httpVersion = append(httpVersion, useragent.VersionInfo{Name: "arch", Version: runtime.GOARCH})
|
|
|
|
dockerUserAgent = useragent.AppendVersions("", httpVersion...)
|
|
|
|
if runtime.GOOS != "linux" {
|
|
V2Only = true
|
|
}
|
|
}
|
|
|
|
func newTLSConfig(hostname string, isSecure bool) (*tls.Config, error) {
|
|
// PreferredServerCipherSuites should have no effect
|
|
tlsConfig := tlsconfig.ServerDefault
|
|
|
|
tlsConfig.InsecureSkipVerify = !isSecure
|
|
|
|
if isSecure && CertsDir != "" {
|
|
hostDir := filepath.Join(CertsDir, cleanPath(hostname))
|
|
logrus.Debugf("hostDir: %s", hostDir)
|
|
if err := ReadCertsDirectory(&tlsConfig, hostDir); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return &tlsConfig, nil
|
|
}
|
|
|
|
func hasFile(files []os.FileInfo, name string) bool {
|
|
for _, f := range files {
|
|
if f.Name() == name {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ReadCertsDirectory reads the directory for TLS certificates
|
|
// including roots and certificate pairs and updates the
|
|
// provided TLS configuration.
|
|
func ReadCertsDirectory(tlsConfig *tls.Config, directory string) error {
|
|
fs, err := ioutil.ReadDir(directory)
|
|
if err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
|
|
for _, f := range fs {
|
|
if strings.HasSuffix(f.Name(), ".crt") {
|
|
if tlsConfig.RootCAs == nil {
|
|
// TODO(dmcgowan): Copy system pool
|
|
tlsConfig.RootCAs = x509.NewCertPool()
|
|
}
|
|
logrus.Debugf("crt: %s", filepath.Join(directory, f.Name()))
|
|
data, err := ioutil.ReadFile(filepath.Join(directory, f.Name()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tlsConfig.RootCAs.AppendCertsFromPEM(data)
|
|
}
|
|
if strings.HasSuffix(f.Name(), ".cert") {
|
|
certName := f.Name()
|
|
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 certificate %s", keyName, certName)
|
|
}
|
|
cert, err := tls.LoadX509KeyPair(filepath.Join(directory, certName), filepath.Join(directory, keyName))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tlsConfig.Certificates = append(tlsConfig.Certificates, cert)
|
|
}
|
|
if strings.HasSuffix(f.Name(), ".key") {
|
|
keyName := f.Name()
|
|
certName := keyName[:len(keyName)-4] + ".cert"
|
|
logrus.Debugf("key: %s", filepath.Join(directory, f.Name()))
|
|
if !hasFile(fs, certName) {
|
|
return fmt.Errorf("Missing certificate %s for key %s", certName, keyName)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// DockerHeaders returns request modifiers that ensure requests have
|
|
// the User-Agent header set to dockerUserAgent and that metaHeaders
|
|
// are added.
|
|
func DockerHeaders(metaHeaders http.Header) []transport.RequestModifier {
|
|
modifiers := []transport.RequestModifier{
|
|
transport.NewHeaderRequestModifier(http.Header{"User-Agent": []string{dockerUserAgent}}),
|
|
}
|
|
if metaHeaders != nil {
|
|
modifiers = append(modifiers, transport.NewHeaderRequestModifier(metaHeaders))
|
|
}
|
|
return modifiers
|
|
}
|
|
|
|
// HTTPClient returns a HTTP client structure which uses the given transport
|
|
// and contains the necessary headers for redirected requests
|
|
func HTTPClient(transport http.RoundTripper) *http.Client {
|
|
return &http.Client{
|
|
Transport: transport,
|
|
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 via != nil && 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
|
|
}
|
|
|
|
// ShouldV2Fallback returns true if this error is a reason to fall back to v1.
|
|
func ShouldV2Fallback(err errcode.Error) bool {
|
|
switch err.Code {
|
|
case errcode.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// ErrNoSupport is an error type used for errors indicating that an operation
|
|
// is not supported. It encapsulates a more specific error.
|
|
type ErrNoSupport struct{ Err error }
|
|
|
|
func (e ErrNoSupport) Error() string {
|
|
if e.Err == nil {
|
|
return "not supported"
|
|
}
|
|
return e.Err.Error()
|
|
}
|
|
|
|
// ContinueOnError returns true if we should fallback to the next endpoint
|
|
// as a result of this error.
|
|
func ContinueOnError(err error) bool {
|
|
switch v := err.(type) {
|
|
case errcode.Errors:
|
|
if len(v) == 0 {
|
|
return true
|
|
}
|
|
return ContinueOnError(v[0])
|
|
case ErrNoSupport:
|
|
return ContinueOnError(v.Err)
|
|
case errcode.Error:
|
|
return ShouldV2Fallback(v)
|
|
case *client.UnexpectedHTTPResponseError:
|
|
return true
|
|
case error:
|
|
return !strings.Contains(err.Error(), strings.ToLower(syscall.ENOSPC.Error()))
|
|
}
|
|
// let's be nice and fallback if the error is a completely
|
|
// unexpected one.
|
|
// If new errors have to be handled in some way, please
|
|
// add them to the switch above.
|
|
return true
|
|
}
|
|
|
|
// NewTransport returns a new HTTP transport. If tlsConfig is nil, it uses the
|
|
// default TLS configuration.
|
|
func NewTransport(tlsConfig *tls.Config) *http.Transport {
|
|
if tlsConfig == nil {
|
|
var cfg = tlsconfig.ServerDefault
|
|
tlsConfig = &cfg
|
|
}
|
|
return &http.Transport{
|
|
Proxy: http.ProxyFromEnvironment,
|
|
Dial: (&net.Dialer{
|
|
Timeout: 30 * time.Second,
|
|
KeepAlive: 30 * time.Second,
|
|
DualStack: true,
|
|
}).Dial,
|
|
TLSHandshakeTimeout: 10 * time.Second,
|
|
TLSClientConfig: tlsConfig,
|
|
// TODO(dmcgowan): Call close idle connections when complete and use keep alive
|
|
DisableKeepAlives: true,
|
|
}
|
|
}
|