distribution/docs/registry.go
Aaron Lehmann 693eb14e73 Allow v1 protocol fallback when pulling all tags from a repository unknown to v2 registry
This is a followup to #18839. That PR relaxed the fallback logic so that
if a manifest doesn't exist on v2, or the user is unauthorized to access
it, we try again with the v1 protocol. A similar special case is needed
for "pull all tags" (docker pull -a). If the v2 registry doesn't
recognize the repository, or doesn't allow the user to access it, we
should fall back to v1 and try to pull all tags from the v1 registry.
Conversely, if the v2 registry does allow us to list the tags, there
should be no fallback, even if there are errors pulling those tags.

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>
2015-12-23 15:21:43 -08:00

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, v2.ErrorCodeNameUnknown:
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,
}
}