Update graph to use vendored distribution client for the v2 codepath
Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan) Signed-off-by: Tibor Vass <tibor@docker.com>
This commit is contained in:
parent
950cf586c8
commit
7fed379d95
11 changed files with 320 additions and 634 deletions
|
@ -125,7 +125,7 @@ func loginV1(authConfig *cliconfig.AuthConfig, registryEndpoint *Endpoint) (stri
|
||||||
return "", fmt.Errorf("Server Error: Server Address not set.")
|
return "", fmt.Errorf("Server Error: Server Address not set.")
|
||||||
}
|
}
|
||||||
|
|
||||||
loginAgainstOfficialIndex := serverAddress == IndexServerAddress()
|
loginAgainstOfficialIndex := serverAddress == INDEXSERVER
|
||||||
|
|
||||||
// to avoid sending the server address to the server it should be removed before being marshalled
|
// to avoid sending the server address to the server it should be removed before being marshalled
|
||||||
authCopy := *authConfig
|
authCopy := *authConfig
|
||||||
|
|
|
@ -37,7 +37,7 @@ func setupTempConfigFile() (*cliconfig.ConfigFile, error) {
|
||||||
root = filepath.Join(root, cliconfig.CONFIGFILE)
|
root = filepath.Join(root, cliconfig.CONFIGFILE)
|
||||||
configFile := cliconfig.NewConfigFile(root)
|
configFile := cliconfig.NewConfigFile(root)
|
||||||
|
|
||||||
for _, registry := range []string{"testIndex", IndexServerAddress()} {
|
for _, registry := range []string{"testIndex", INDEXSERVER} {
|
||||||
configFile.AuthConfigs[registry] = cliconfig.AuthConfig{
|
configFile.AuthConfigs[registry] = cliconfig.AuthConfig{
|
||||||
Username: "docker-user",
|
Username: "docker-user",
|
||||||
Password: "docker-pass",
|
Password: "docker-pass",
|
||||||
|
@ -82,7 +82,7 @@ func TestResolveAuthConfigIndexServer(t *testing.T) {
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(configFile.Filename())
|
defer os.RemoveAll(configFile.Filename())
|
||||||
|
|
||||||
indexConfig := configFile.AuthConfigs[IndexServerAddress()]
|
indexConfig := configFile.AuthConfigs[INDEXSERVER]
|
||||||
|
|
||||||
officialIndex := &IndexInfo{
|
officialIndex := &IndexInfo{
|
||||||
Official: true,
|
Official: true,
|
||||||
|
@ -92,10 +92,10 @@ func TestResolveAuthConfigIndexServer(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
resolved := ResolveAuthConfig(configFile, officialIndex)
|
resolved := ResolveAuthConfig(configFile, officialIndex)
|
||||||
assertEqual(t, resolved, indexConfig, "Expected ResolveAuthConfig to return IndexServerAddress()")
|
assertEqual(t, resolved, indexConfig, "Expected ResolveAuthConfig to return INDEXSERVER")
|
||||||
|
|
||||||
resolved = ResolveAuthConfig(configFile, privateIndex)
|
resolved = ResolveAuthConfig(configFile, privateIndex)
|
||||||
assertNotEqual(t, resolved, indexConfig, "Expected ResolveAuthConfig to not return IndexServerAddress()")
|
assertNotEqual(t, resolved, indexConfig, "Expected ResolveAuthConfig to not return INDEXSERVER")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolveAuthConfigFullURL(t *testing.T) {
|
func TestResolveAuthConfigFullURL(t *testing.T) {
|
||||||
|
@ -120,7 +120,7 @@ func TestResolveAuthConfigFullURL(t *testing.T) {
|
||||||
Password: "baz-pass",
|
Password: "baz-pass",
|
||||||
Email: "baz@example.com",
|
Email: "baz@example.com",
|
||||||
}
|
}
|
||||||
configFile.AuthConfigs[IndexServerAddress()] = officialAuth
|
configFile.AuthConfigs[INDEXSERVER] = officialAuth
|
||||||
|
|
||||||
expectedAuths := map[string]cliconfig.AuthConfig{
|
expectedAuths := map[string]cliconfig.AuthConfig{
|
||||||
"registry.example.com": registryAuth,
|
"registry.example.com": registryAuth,
|
||||||
|
|
|
@ -21,9 +21,16 @@ type Options struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
DEFAULT_NAMESPACE = "docker.io"
|
||||||
|
DEFAULT_V2_REGISTRY = "https://registry-1.docker.io"
|
||||||
|
DEFAULT_REGISTRY_VERSION_HEADER = "Docker-Distribution-Api-Version"
|
||||||
|
DEFAULT_V1_REGISTRY = "https://index.docker.io"
|
||||||
|
|
||||||
|
CERTS_DIR = "/etc/docker/certs.d"
|
||||||
|
|
||||||
// Only used for user auth + account creation
|
// Only used for user auth + account creation
|
||||||
INDEXSERVER = "https://index.docker.io/v1/"
|
REGISTRYSERVER = DEFAULT_V2_REGISTRY
|
||||||
REGISTRYSERVER = "https://registry-1.docker.io/v2/"
|
INDEXSERVER = DEFAULT_V1_REGISTRY + "/v1/"
|
||||||
INDEXNAME = "docker.io"
|
INDEXNAME = "docker.io"
|
||||||
|
|
||||||
// INDEXSERVER = "https://registry-stage.hub.docker.com/v1/"
|
// INDEXSERVER = "https://registry-stage.hub.docker.com/v1/"
|
||||||
|
@ -34,14 +41,6 @@ var (
|
||||||
emptyServiceConfig = NewServiceConfig(nil)
|
emptyServiceConfig = NewServiceConfig(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
func IndexServerAddress() string {
|
|
||||||
return INDEXSERVER
|
|
||||||
}
|
|
||||||
|
|
||||||
func IndexServerName() string {
|
|
||||||
return INDEXNAME
|
|
||||||
}
|
|
||||||
|
|
||||||
// InstallFlags adds command-line options to the top-level flag parser for
|
// InstallFlags adds command-line options to the top-level flag parser for
|
||||||
// the current process.
|
// the current process.
|
||||||
func (options *Options) InstallFlags() {
|
func (options *Options) InstallFlags() {
|
||||||
|
@ -72,6 +71,7 @@ func (ipnet *netIPNet) UnmarshalJSON(b []byte) (err error) {
|
||||||
type ServiceConfig struct {
|
type ServiceConfig struct {
|
||||||
InsecureRegistryCIDRs []*netIPNet `json:"InsecureRegistryCIDRs"`
|
InsecureRegistryCIDRs []*netIPNet `json:"InsecureRegistryCIDRs"`
|
||||||
IndexConfigs map[string]*IndexInfo `json:"IndexConfigs"`
|
IndexConfigs map[string]*IndexInfo `json:"IndexConfigs"`
|
||||||
|
Mirrors []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServiceConfig returns a new instance of ServiceConfig
|
// NewServiceConfig returns a new instance of ServiceConfig
|
||||||
|
@ -93,6 +93,9 @@ func NewServiceConfig(options *Options) *ServiceConfig {
|
||||||
config := &ServiceConfig{
|
config := &ServiceConfig{
|
||||||
InsecureRegistryCIDRs: make([]*netIPNet, 0),
|
InsecureRegistryCIDRs: make([]*netIPNet, 0),
|
||||||
IndexConfigs: make(map[string]*IndexInfo, 0),
|
IndexConfigs: make(map[string]*IndexInfo, 0),
|
||||||
|
// Hack: Bypass setting the mirrors to IndexConfigs since they are going away
|
||||||
|
// and Mirrors are only for the official registry anyways.
|
||||||
|
Mirrors: options.Mirrors.GetAll(),
|
||||||
}
|
}
|
||||||
// Split --insecure-registry into CIDR and registry-specific settings.
|
// Split --insecure-registry into CIDR and registry-specific settings.
|
||||||
for _, r := range options.InsecureRegistries.GetAll() {
|
for _, r := range options.InsecureRegistries.GetAll() {
|
||||||
|
@ -113,9 +116,9 @@ func NewServiceConfig(options *Options) *ServiceConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure public registry.
|
// Configure public registry.
|
||||||
config.IndexConfigs[IndexServerName()] = &IndexInfo{
|
config.IndexConfigs[INDEXNAME] = &IndexInfo{
|
||||||
Name: IndexServerName(),
|
Name: INDEXNAME,
|
||||||
Mirrors: options.Mirrors.GetAll(),
|
Mirrors: config.Mirrors,
|
||||||
Secure: true,
|
Secure: true,
|
||||||
Official: true,
|
Official: true,
|
||||||
}
|
}
|
||||||
|
@ -193,8 +196,8 @@ func ValidateMirror(val string) (string, error) {
|
||||||
// ValidateIndexName validates an index name.
|
// ValidateIndexName validates an index name.
|
||||||
func ValidateIndexName(val string) (string, error) {
|
func ValidateIndexName(val string) (string, error) {
|
||||||
// 'index.docker.io' => 'docker.io'
|
// 'index.docker.io' => 'docker.io'
|
||||||
if val == "index."+IndexServerName() {
|
if val == "index."+INDEXNAME {
|
||||||
val = IndexServerName()
|
val = INDEXNAME
|
||||||
}
|
}
|
||||||
if strings.HasPrefix(val, "-") || strings.HasSuffix(val, "-") {
|
if strings.HasPrefix(val, "-") || strings.HasSuffix(val, "-") {
|
||||||
return "", fmt.Errorf("Invalid index name (%s). Cannot begin or end with a hyphen.", val)
|
return "", fmt.Errorf("Invalid index name (%s). Cannot begin or end with a hyphen.", val)
|
||||||
|
@ -264,7 +267,7 @@ func (config *ServiceConfig) NewIndexInfo(indexName string) (*IndexInfo, error)
|
||||||
// index as the AuthConfig key, and uses the (host)name[:port] for private indexes.
|
// index as the AuthConfig key, and uses the (host)name[:port] for private indexes.
|
||||||
func (index *IndexInfo) GetAuthConfigKey() string {
|
func (index *IndexInfo) GetAuthConfigKey() string {
|
||||||
if index.Official {
|
if index.Official {
|
||||||
return IndexServerAddress()
|
return INDEXSERVER
|
||||||
}
|
}
|
||||||
return index.Name
|
return index.Name
|
||||||
}
|
}
|
||||||
|
@ -277,7 +280,7 @@ func splitReposName(reposName string) (string, string) {
|
||||||
!strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") {
|
!strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") {
|
||||||
// This is a Docker Index repos (ex: samalba/hipache or ubuntu)
|
// This is a Docker Index repos (ex: samalba/hipache or ubuntu)
|
||||||
// 'docker.io'
|
// 'docker.io'
|
||||||
indexName = IndexServerName()
|
indexName = INDEXNAME
|
||||||
remoteName = reposName
|
remoteName = reposName
|
||||||
} else {
|
} else {
|
||||||
indexName = nameParts[0]
|
indexName = nameParts[0]
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package registry
|
package registry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -12,6 +13,7 @@ import (
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
"github.com/docker/distribution/registry/api/v2"
|
"github.com/docker/distribution/registry/api/v2"
|
||||||
"github.com/docker/distribution/registry/client/transport"
|
"github.com/docker/distribution/registry/client/transport"
|
||||||
|
"github.com/docker/docker/pkg/tlsconfig"
|
||||||
)
|
)
|
||||||
|
|
||||||
// for mocking in unit tests
|
// for mocking in unit tests
|
||||||
|
@ -44,7 +46,9 @@ func scanForAPIVersion(address string) (string, APIVersion) {
|
||||||
// NewEndpoint parses the given address to return a registry endpoint.
|
// NewEndpoint parses the given address to return a registry endpoint.
|
||||||
func NewEndpoint(index *IndexInfo, metaHeaders http.Header) (*Endpoint, error) {
|
func NewEndpoint(index *IndexInfo, metaHeaders http.Header) (*Endpoint, error) {
|
||||||
// *TODO: Allow per-registry configuration of endpoints.
|
// *TODO: Allow per-registry configuration of endpoints.
|
||||||
endpoint, err := newEndpoint(index.GetAuthConfigKey(), index.Secure, metaHeaders)
|
tlsConfig := tlsconfig.ServerDefault
|
||||||
|
tlsConfig.InsecureSkipVerify = !index.Secure
|
||||||
|
endpoint, err := newEndpoint(index.GetAuthConfigKey(), &tlsConfig, metaHeaders)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -82,7 +86,7 @@ func validateEndpoint(endpoint *Endpoint) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newEndpoint(address string, secure bool, metaHeaders http.Header) (*Endpoint, error) {
|
func newEndpoint(address string, tlsConfig *tls.Config, metaHeaders http.Header) (*Endpoint, error) {
|
||||||
var (
|
var (
|
||||||
endpoint = new(Endpoint)
|
endpoint = new(Endpoint)
|
||||||
trimmedAddress string
|
trimmedAddress string
|
||||||
|
@ -93,13 +97,16 @@ func newEndpoint(address string, secure bool, metaHeaders http.Header) (*Endpoin
|
||||||
address = "https://" + address
|
address = "https://" + address
|
||||||
}
|
}
|
||||||
|
|
||||||
|
endpoint.IsSecure = (tlsConfig == nil || !tlsConfig.InsecureSkipVerify)
|
||||||
|
|
||||||
trimmedAddress, endpoint.Version = scanForAPIVersion(address)
|
trimmedAddress, endpoint.Version = scanForAPIVersion(address)
|
||||||
|
|
||||||
if endpoint.URL, err = url.Parse(trimmedAddress); err != nil {
|
if endpoint.URL, err = url.Parse(trimmedAddress); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
endpoint.IsSecure = secure
|
|
||||||
tr := NewTransport(ConnectTimeout, endpoint.IsSecure)
|
// TODO(tiborvass): make sure a ConnectTimeout transport is used
|
||||||
|
tr := NewTransport(tlsConfig)
|
||||||
endpoint.client = HTTPClient(transport.NewTransport(tr, DockerHeaders(metaHeaders)...))
|
endpoint.client = HTTPClient(transport.NewTransport(tr, DockerHeaders(metaHeaders)...))
|
||||||
return endpoint, nil
|
return endpoint, nil
|
||||||
}
|
}
|
||||||
|
@ -166,7 +173,7 @@ func (e *Endpoint) Ping() (RegistryInfo, error) {
|
||||||
func (e *Endpoint) pingV1() (RegistryInfo, error) {
|
func (e *Endpoint) pingV1() (RegistryInfo, error) {
|
||||||
logrus.Debugf("attempting v1 ping for registry endpoint %s", e)
|
logrus.Debugf("attempting v1 ping for registry endpoint %s", e)
|
||||||
|
|
||||||
if e.String() == IndexServerAddress() {
|
if e.String() == INDEXSERVER {
|
||||||
// Skip the check, we know this one is valid
|
// Skip the check, we know this one is valid
|
||||||
// (and we never want to fallback to http in case of error)
|
// (and we never want to fallback to http in case of error)
|
||||||
return RegistryInfo{Standalone: false}, nil
|
return RegistryInfo{Standalone: false}, nil
|
||||||
|
|
|
@ -12,14 +12,14 @@ func TestEndpointParse(t *testing.T) {
|
||||||
str string
|
str string
|
||||||
expected string
|
expected string
|
||||||
}{
|
}{
|
||||||
{IndexServerAddress(), IndexServerAddress()},
|
{INDEXSERVER, INDEXSERVER},
|
||||||
{"http://0.0.0.0:5000/v1/", "http://0.0.0.0:5000/v1/"},
|
{"http://0.0.0.0:5000/v1/", "http://0.0.0.0:5000/v1/"},
|
||||||
{"http://0.0.0.0:5000/v2/", "http://0.0.0.0:5000/v2/"},
|
{"http://0.0.0.0:5000/v2/", "http://0.0.0.0:5000/v2/"},
|
||||||
{"http://0.0.0.0:5000", "http://0.0.0.0:5000/v0/"},
|
{"http://0.0.0.0:5000", "http://0.0.0.0:5000/v0/"},
|
||||||
{"0.0.0.0:5000", "https://0.0.0.0:5000/v0/"},
|
{"0.0.0.0:5000", "https://0.0.0.0:5000/v0/"},
|
||||||
}
|
}
|
||||||
for _, td := range testData {
|
for _, td := range testData {
|
||||||
e, err := newEndpoint(td.str, false, nil)
|
e, err := newEndpoint(td.str, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("%q: %s", td.str, err)
|
t.Errorf("%q: %s", td.str, err)
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ func TestValidateEndpointAmbiguousAPIVersion(t *testing.T) {
|
||||||
testEndpoint := Endpoint{
|
testEndpoint := Endpoint{
|
||||||
URL: testServerURL,
|
URL: testServerURL,
|
||||||
Version: APIVersionUnknown,
|
Version: APIVersionUnknown,
|
||||||
client: HTTPClient(NewTransport(ConnectTimeout, false)),
|
client: HTTPClient(NewTransport(nil)),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = validateEndpoint(&testEndpoint); err != nil {
|
if err = validateEndpoint(&testEndpoint); err != nil {
|
||||||
|
|
186
docs/registry.go
186
docs/registry.go
|
@ -2,25 +2,20 @@ package registry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/distribution/registry/api/errcode"
|
||||||
|
"github.com/docker/distribution/registry/api/v2"
|
||||||
"github.com/docker/distribution/registry/client/transport"
|
"github.com/docker/distribution/registry/client/transport"
|
||||||
"github.com/docker/docker/autogen/dockerversion"
|
"github.com/docker/docker/autogen/dockerversion"
|
||||||
"github.com/docker/docker/pkg/parsers/kernel"
|
"github.com/docker/docker/pkg/parsers/kernel"
|
||||||
"github.com/docker/docker/pkg/timeoutconn"
|
|
||||||
"github.com/docker/docker/pkg/tlsconfig"
|
"github.com/docker/docker/pkg/tlsconfig"
|
||||||
"github.com/docker/docker/pkg/useragent"
|
"github.com/docker/docker/pkg/useragent"
|
||||||
)
|
)
|
||||||
|
@ -57,25 +52,7 @@ func init() {
|
||||||
dockerUserAgent = useragent.AppendVersions("", httpVersion...)
|
dockerUserAgent = useragent.AppendVersions("", httpVersion...)
|
||||||
}
|
}
|
||||||
|
|
||||||
type httpsRequestModifier struct {
|
func hasFile(files []os.FileInfo, name string) bool {
|
||||||
mu sync.Mutex
|
|
||||||
tlsConfig *tls.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
// DRAGONS(tiborvass): If someone wonders why do we set tlsconfig in a roundtrip,
|
|
||||||
// it's because it's so as to match the current behavior in master: we generate the
|
|
||||||
// certpool on every-goddam-request. It's not great, but it allows people to just put
|
|
||||||
// the certs in /etc/docker/certs.d/.../ and let docker "pick it up" immediately. Would
|
|
||||||
// prefer an fsnotify implementation, but that was out of scope of my refactoring.
|
|
||||||
func (m *httpsRequestModifier) ModifyRequest(req *http.Request) error {
|
|
||||||
var (
|
|
||||||
roots *x509.CertPool
|
|
||||||
certs []tls.Certificate
|
|
||||||
hostDir string
|
|
||||||
)
|
|
||||||
|
|
||||||
if req.URL.Scheme == "https" {
|
|
||||||
hasFile := func(files []os.FileInfo, name string) bool {
|
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
if f.Name() == name {
|
if f.Name() == name {
|
||||||
return true
|
return true
|
||||||
|
@ -84,110 +61,6 @@ func (m *httpsRequestModifier) ModifyRequest(req *http.Request) error {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if runtime.GOOS == "windows" {
|
|
||||||
hostDir = path.Join(os.TempDir(), "/docker/certs.d", req.URL.Host)
|
|
||||||
} else {
|
|
||||||
hostDir = path.Join("/etc/docker/certs.d", req.URL.Host)
|
|
||||||
}
|
|
||||||
logrus.Debugf("hostDir: %s", hostDir)
|
|
||||||
fs, err := ioutil.ReadDir(hostDir)
|
|
||||||
if err != nil && !os.IsNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, f := range fs {
|
|
||||||
if strings.HasSuffix(f.Name(), ".crt") {
|
|
||||||
if roots == nil {
|
|
||||||
roots = x509.NewCertPool()
|
|
||||||
}
|
|
||||||
logrus.Debugf("crt: %s", hostDir+"/"+f.Name())
|
|
||||||
data, err := ioutil.ReadFile(filepath.Join(hostDir, f.Name()))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
roots.AppendCertsFromPEM(data)
|
|
||||||
}
|
|
||||||
if strings.HasSuffix(f.Name(), ".cert") {
|
|
||||||
certName := f.Name()
|
|
||||||
keyName := certName[:len(certName)-5] + ".key"
|
|
||||||
logrus.Debugf("cert: %s", hostDir+"/"+f.Name())
|
|
||||||
if !hasFile(fs, keyName) {
|
|
||||||
return fmt.Errorf("Missing key %s for certificate %s", keyName, certName)
|
|
||||||
}
|
|
||||||
cert, err := tls.LoadX509KeyPair(filepath.Join(hostDir, certName), path.Join(hostDir, keyName))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
certs = append(certs, cert)
|
|
||||||
}
|
|
||||||
if strings.HasSuffix(f.Name(), ".key") {
|
|
||||||
keyName := f.Name()
|
|
||||||
certName := keyName[:len(keyName)-4] + ".cert"
|
|
||||||
logrus.Debugf("key: %s", hostDir+"/"+f.Name())
|
|
||||||
if !hasFile(fs, certName) {
|
|
||||||
return fmt.Errorf("Missing certificate %s for key %s", certName, keyName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m.mu.Lock()
|
|
||||||
m.tlsConfig.RootCAs = roots
|
|
||||||
m.tlsConfig.Certificates = certs
|
|
||||||
m.mu.Unlock()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewTransport(timeout TimeoutType, secure bool) http.RoundTripper {
|
|
||||||
tlsConfig := &tls.Config{
|
|
||||||
// Avoid fallback to SSL protocols < TLS1.0
|
|
||||||
MinVersion: tls.VersionTLS10,
|
|
||||||
InsecureSkipVerify: !secure,
|
|
||||||
CipherSuites: tlsconfig.DefaultServerAcceptedCiphers,
|
|
||||||
}
|
|
||||||
|
|
||||||
tr := &http.Transport{
|
|
||||||
DisableKeepAlives: true,
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
TLSClientConfig: tlsConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
switch timeout {
|
|
||||||
case ConnectTimeout:
|
|
||||||
tr.Dial = func(proto string, addr string) (net.Conn, error) {
|
|
||||||
// Set the connect timeout to 30 seconds to allow for slower connection
|
|
||||||
// times...
|
|
||||||
d := net.Dialer{Timeout: 30 * time.Second, DualStack: true}
|
|
||||||
|
|
||||||
conn, err := d.Dial(proto, addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Set the recv timeout to 10 seconds
|
|
||||||
conn.SetDeadline(time.Now().Add(10 * time.Second))
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
case ReceiveTimeout:
|
|
||||||
tr.Dial = func(proto string, addr string) (net.Conn, error) {
|
|
||||||
d := net.Dialer{DualStack: true}
|
|
||||||
|
|
||||||
conn, err := d.Dial(proto, addr)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
conn = timeoutconn.New(conn, 1*time.Minute)
|
|
||||||
return conn, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if secure {
|
|
||||||
// note: httpsTransport also handles http transport
|
|
||||||
// but for HTTPS, it sets up the certs
|
|
||||||
return transport.NewTransport(tr, &httpsRequestModifier{tlsConfig: tlsConfig})
|
|
||||||
}
|
|
||||||
|
|
||||||
return tr
|
|
||||||
}
|
|
||||||
|
|
||||||
// DockerHeaders returns request modifiers that ensure requests have
|
// DockerHeaders returns request modifiers that ensure requests have
|
||||||
// the User-Agent header set to dockerUserAgent and that metaHeaders
|
// the User-Agent header set to dockerUserAgent and that metaHeaders
|
||||||
// are added.
|
// are added.
|
||||||
|
@ -202,10 +75,6 @@ func DockerHeaders(metaHeaders http.Header) []transport.RequestModifier {
|
||||||
}
|
}
|
||||||
|
|
||||||
func HTTPClient(transport http.RoundTripper) *http.Client {
|
func HTTPClient(transport http.RoundTripper) *http.Client {
|
||||||
if transport == nil {
|
|
||||||
transport = NewTransport(ConnectTimeout, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &http.Client{
|
return &http.Client{
|
||||||
Transport: transport,
|
Transport: transport,
|
||||||
CheckRedirect: AddRequiredHeadersToRedirectedRequests,
|
CheckRedirect: AddRequiredHeadersToRedirectedRequests,
|
||||||
|
@ -245,3 +114,52 @@ func AddRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Reque
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shouldV2Fallback(err errcode.Error) bool {
|
||||||
|
logrus.Debugf("v2 error: %T %v", err, err)
|
||||||
|
switch err.Code {
|
||||||
|
case v2.ErrorCodeUnauthorized, v2.ErrorCodeManifestUnknown:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
type ErrNoSupport struct{ Err error }
|
||||||
|
|
||||||
|
func (e ErrNoSupport) Error() string {
|
||||||
|
if e.Err == nil {
|
||||||
|
return "not supported"
|
||||||
|
}
|
||||||
|
return e.Err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ContinueOnError(err error) bool {
|
||||||
|
switch v := err.(type) {
|
||||||
|
case errcode.Errors:
|
||||||
|
return ContinueOnError(v[0])
|
||||||
|
case ErrNoSupport:
|
||||||
|
return ContinueOnError(v.Err)
|
||||||
|
case errcode.Error:
|
||||||
|
return shouldV2Fallback(v)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -165,7 +165,7 @@ func makeHttpsIndex(req string) *IndexInfo {
|
||||||
|
|
||||||
func makePublicIndex() *IndexInfo {
|
func makePublicIndex() *IndexInfo {
|
||||||
index := &IndexInfo{
|
index := &IndexInfo{
|
||||||
Name: IndexServerAddress(),
|
Name: INDEXSERVER,
|
||||||
Secure: true,
|
Secure: true,
|
||||||
Official: true,
|
Official: true,
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ func spawnTestRegistrySession(t *testing.T) *Session {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
var tr http.RoundTripper = debugTransport{NewTransport(ReceiveTimeout, endpoint.IsSecure), t.Log}
|
var tr http.RoundTripper = debugTransport{NewTransport(nil), t.Log}
|
||||||
tr = transport.NewTransport(AuthTransport(tr, authConfig, false), DockerHeaders(nil)...)
|
tr = transport.NewTransport(AuthTransport(tr, authConfig, false), DockerHeaders(nil)...)
|
||||||
client := HTTPClient(tr)
|
client := HTTPClient(tr)
|
||||||
r, err := NewSession(client, authConfig, endpoint)
|
r, err := NewSession(client, authConfig, endpoint)
|
||||||
|
@ -332,7 +332,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
expectedRepoInfos := map[string]RepositoryInfo{
|
expectedRepoInfos := map[string]RepositoryInfo{
|
||||||
"fooo/bar": {
|
"fooo/bar": {
|
||||||
Index: &IndexInfo{
|
Index: &IndexInfo{
|
||||||
Name: IndexServerName(),
|
Name: INDEXNAME,
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
RemoteName: "fooo/bar",
|
RemoteName: "fooo/bar",
|
||||||
|
@ -342,7 +342,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
},
|
},
|
||||||
"library/ubuntu": {
|
"library/ubuntu": {
|
||||||
Index: &IndexInfo{
|
Index: &IndexInfo{
|
||||||
Name: IndexServerName(),
|
Name: INDEXNAME,
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
RemoteName: "library/ubuntu",
|
RemoteName: "library/ubuntu",
|
||||||
|
@ -352,7 +352,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
},
|
},
|
||||||
"nonlibrary/ubuntu": {
|
"nonlibrary/ubuntu": {
|
||||||
Index: &IndexInfo{
|
Index: &IndexInfo{
|
||||||
Name: IndexServerName(),
|
Name: INDEXNAME,
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
RemoteName: "nonlibrary/ubuntu",
|
RemoteName: "nonlibrary/ubuntu",
|
||||||
|
@ -362,7 +362,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
},
|
},
|
||||||
"ubuntu": {
|
"ubuntu": {
|
||||||
Index: &IndexInfo{
|
Index: &IndexInfo{
|
||||||
Name: IndexServerName(),
|
Name: INDEXNAME,
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
RemoteName: "library/ubuntu",
|
RemoteName: "library/ubuntu",
|
||||||
|
@ -372,7 +372,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
},
|
},
|
||||||
"other/library": {
|
"other/library": {
|
||||||
Index: &IndexInfo{
|
Index: &IndexInfo{
|
||||||
Name: IndexServerName(),
|
Name: INDEXNAME,
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
RemoteName: "other/library",
|
RemoteName: "other/library",
|
||||||
|
@ -480,9 +480,9 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
CanonicalName: "localhost/privatebase",
|
CanonicalName: "localhost/privatebase",
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
IndexServerName() + "/public/moonbase": {
|
INDEXNAME + "/public/moonbase": {
|
||||||
Index: &IndexInfo{
|
Index: &IndexInfo{
|
||||||
Name: IndexServerName(),
|
Name: INDEXNAME,
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
RemoteName: "public/moonbase",
|
RemoteName: "public/moonbase",
|
||||||
|
@ -490,19 +490,9 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
CanonicalName: "docker.io/public/moonbase",
|
CanonicalName: "docker.io/public/moonbase",
|
||||||
Official: false,
|
Official: false,
|
||||||
},
|
},
|
||||||
"index." + IndexServerName() + "/public/moonbase": {
|
"index." + INDEXNAME + "/public/moonbase": {
|
||||||
Index: &IndexInfo{
|
Index: &IndexInfo{
|
||||||
Name: IndexServerName(),
|
Name: INDEXNAME,
|
||||||
Official: true,
|
|
||||||
},
|
|
||||||
RemoteName: "public/moonbase",
|
|
||||||
LocalName: "public/moonbase",
|
|
||||||
CanonicalName: "docker.io/public/moonbase",
|
|
||||||
Official: false,
|
|
||||||
},
|
|
||||||
IndexServerName() + "/public/moonbase": {
|
|
||||||
Index: &IndexInfo{
|
|
||||||
Name: IndexServerName(),
|
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
RemoteName: "public/moonbase",
|
RemoteName: "public/moonbase",
|
||||||
|
@ -512,7 +502,7 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
},
|
},
|
||||||
"ubuntu-12.04-base": {
|
"ubuntu-12.04-base": {
|
||||||
Index: &IndexInfo{
|
Index: &IndexInfo{
|
||||||
Name: IndexServerName(),
|
Name: INDEXNAME,
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
RemoteName: "library/ubuntu-12.04-base",
|
RemoteName: "library/ubuntu-12.04-base",
|
||||||
|
@ -520,9 +510,9 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
CanonicalName: "docker.io/library/ubuntu-12.04-base",
|
CanonicalName: "docker.io/library/ubuntu-12.04-base",
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
IndexServerName() + "/ubuntu-12.04-base": {
|
INDEXNAME + "/ubuntu-12.04-base": {
|
||||||
Index: &IndexInfo{
|
Index: &IndexInfo{
|
||||||
Name: IndexServerName(),
|
Name: INDEXNAME,
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
RemoteName: "library/ubuntu-12.04-base",
|
RemoteName: "library/ubuntu-12.04-base",
|
||||||
|
@ -530,19 +520,9 @@ func TestParseRepositoryInfo(t *testing.T) {
|
||||||
CanonicalName: "docker.io/library/ubuntu-12.04-base",
|
CanonicalName: "docker.io/library/ubuntu-12.04-base",
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
IndexServerName() + "/ubuntu-12.04-base": {
|
"index." + INDEXNAME + "/ubuntu-12.04-base": {
|
||||||
Index: &IndexInfo{
|
Index: &IndexInfo{
|
||||||
Name: IndexServerName(),
|
Name: INDEXNAME,
|
||||||
Official: true,
|
|
||||||
},
|
|
||||||
RemoteName: "library/ubuntu-12.04-base",
|
|
||||||
LocalName: "ubuntu-12.04-base",
|
|
||||||
CanonicalName: "docker.io/library/ubuntu-12.04-base",
|
|
||||||
Official: true,
|
|
||||||
},
|
|
||||||
"index." + IndexServerName() + "/ubuntu-12.04-base": {
|
|
||||||
Index: &IndexInfo{
|
|
||||||
Name: IndexServerName(),
|
|
||||||
Official: true,
|
Official: true,
|
||||||
},
|
},
|
||||||
RemoteName: "library/ubuntu-12.04-base",
|
RemoteName: "library/ubuntu-12.04-base",
|
||||||
|
@ -585,14 +565,14 @@ func TestNewIndexInfo(t *testing.T) {
|
||||||
config := NewServiceConfig(nil)
|
config := NewServiceConfig(nil)
|
||||||
noMirrors := make([]string, 0)
|
noMirrors := make([]string, 0)
|
||||||
expectedIndexInfos := map[string]*IndexInfo{
|
expectedIndexInfos := map[string]*IndexInfo{
|
||||||
IndexServerName(): {
|
INDEXNAME: {
|
||||||
Name: IndexServerName(),
|
Name: INDEXNAME,
|
||||||
Official: true,
|
Official: true,
|
||||||
Secure: true,
|
Secure: true,
|
||||||
Mirrors: noMirrors,
|
Mirrors: noMirrors,
|
||||||
},
|
},
|
||||||
"index." + IndexServerName(): {
|
"index." + INDEXNAME: {
|
||||||
Name: IndexServerName(),
|
Name: INDEXNAME,
|
||||||
Official: true,
|
Official: true,
|
||||||
Secure: true,
|
Secure: true,
|
||||||
Mirrors: noMirrors,
|
Mirrors: noMirrors,
|
||||||
|
@ -616,14 +596,14 @@ func TestNewIndexInfo(t *testing.T) {
|
||||||
config = makeServiceConfig(publicMirrors, []string{"example.com"})
|
config = makeServiceConfig(publicMirrors, []string{"example.com"})
|
||||||
|
|
||||||
expectedIndexInfos = map[string]*IndexInfo{
|
expectedIndexInfos = map[string]*IndexInfo{
|
||||||
IndexServerName(): {
|
INDEXNAME: {
|
||||||
Name: IndexServerName(),
|
Name: INDEXNAME,
|
||||||
Official: true,
|
Official: true,
|
||||||
Secure: true,
|
Secure: true,
|
||||||
Mirrors: publicMirrors,
|
Mirrors: publicMirrors,
|
||||||
},
|
},
|
||||||
"index." + IndexServerName(): {
|
"index." + INDEXNAME: {
|
||||||
Name: IndexServerName(),
|
Name: INDEXNAME,
|
||||||
Official: true,
|
Official: true,
|
||||||
Secure: true,
|
Secure: true,
|
||||||
Mirrors: publicMirrors,
|
Mirrors: publicMirrors,
|
||||||
|
@ -880,7 +860,7 @@ func TestIsSecureIndex(t *testing.T) {
|
||||||
insecureRegistries []string
|
insecureRegistries []string
|
||||||
expected bool
|
expected bool
|
||||||
}{
|
}{
|
||||||
{IndexServerName(), nil, true},
|
{INDEXNAME, nil, true},
|
||||||
{"example.com", []string{}, true},
|
{"example.com", []string{}, true},
|
||||||
{"example.com", []string{"example.com"}, false},
|
{"example.com", []string{"example.com"}, false},
|
||||||
{"localhost", []string{"localhost:5000"}, false},
|
{"localhost", []string{"localhost:5000"}, false},
|
||||||
|
|
195
docs/service.go
195
docs/service.go
|
@ -1,9 +1,19 @@
|
||||||
package registry
|
package registry
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Sirupsen/logrus"
|
||||||
|
"github.com/docker/distribution/registry/client/auth"
|
||||||
"github.com/docker/docker/cliconfig"
|
"github.com/docker/docker/cliconfig"
|
||||||
|
"github.com/docker/docker/pkg/tlsconfig"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
@ -25,7 +35,7 @@ func (s *Service) Auth(authConfig *cliconfig.AuthConfig) (string, error) {
|
||||||
addr := authConfig.ServerAddress
|
addr := authConfig.ServerAddress
|
||||||
if addr == "" {
|
if addr == "" {
|
||||||
// Use the official registry address if not specified.
|
// Use the official registry address if not specified.
|
||||||
addr = IndexServerAddress()
|
addr = INDEXSERVER
|
||||||
}
|
}
|
||||||
index, err := s.ResolveIndex(addr)
|
index, err := s.ResolveIndex(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -69,3 +79,186 @@ func (s *Service) ResolveRepository(name string) (*RepositoryInfo, error) {
|
||||||
func (s *Service) ResolveIndex(name string) (*IndexInfo, error) {
|
func (s *Service) ResolveIndex(name string) (*IndexInfo, error) {
|
||||||
return s.Config.NewIndexInfo(name)
|
return s.Config.NewIndexInfo(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type APIEndpoint struct {
|
||||||
|
Mirror bool
|
||||||
|
URL string
|
||||||
|
Version APIVersion
|
||||||
|
Official bool
|
||||||
|
TrimHostname bool
|
||||||
|
TLSConfig *tls.Config
|
||||||
|
VersionHeader string
|
||||||
|
Versions []auth.APIVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e APIEndpoint) ToV1Endpoint(metaHeaders http.Header) (*Endpoint, error) {
|
||||||
|
return newEndpoint(e.URL, e.TLSConfig, metaHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) TlsConfig(hostname string) (*tls.Config, error) {
|
||||||
|
// we construct a client tls config from server defaults
|
||||||
|
// PreferredServerCipherSuites should have no effect
|
||||||
|
tlsConfig := tlsconfig.ServerDefault
|
||||||
|
|
||||||
|
isSecure := s.Config.isSecureIndex(hostname)
|
||||||
|
|
||||||
|
tlsConfig.InsecureSkipVerify = !isSecure
|
||||||
|
|
||||||
|
if isSecure {
|
||||||
|
hasFile := func(files []os.FileInfo, name string) bool {
|
||||||
|
for _, f := range files {
|
||||||
|
if f.Name() == name {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
hostDir := filepath.Join(CERTS_DIR, hostname)
|
||||||
|
logrus.Debugf("hostDir: %s", hostDir)
|
||||||
|
fs, err := ioutil.ReadDir(hostDir)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return nil, 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(hostDir, f.Name()))
|
||||||
|
data, err := ioutil.ReadFile(filepath.Join(hostDir, f.Name()))
|
||||||
|
if err != nil {
|
||||||
|
return nil, 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(hostDir, f.Name()))
|
||||||
|
if !hasFile(fs, keyName) {
|
||||||
|
return nil, fmt.Errorf("Missing key %s for certificate %s", keyName, certName)
|
||||||
|
}
|
||||||
|
cert, err := tls.LoadX509KeyPair(filepath.Join(hostDir, certName), filepath.Join(hostDir, keyName))
|
||||||
|
if err != nil {
|
||||||
|
return nil, 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(hostDir, f.Name()))
|
||||||
|
if !hasFile(fs, certName) {
|
||||||
|
return nil, fmt.Errorf("Missing certificate %s for key %s", certName, keyName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &tlsConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) LookupEndpoints(repoName string) (endpoints []APIEndpoint, err error) {
|
||||||
|
var cfg = tlsconfig.ServerDefault
|
||||||
|
tlsConfig := &cfg
|
||||||
|
if strings.HasPrefix(repoName, DEFAULT_NAMESPACE+"/") {
|
||||||
|
// v2 mirrors
|
||||||
|
for _, mirror := range s.Config.Mirrors {
|
||||||
|
endpoints = append(endpoints, APIEndpoint{
|
||||||
|
URL: mirror,
|
||||||
|
// guess mirrors are v2
|
||||||
|
Version: APIVersion2,
|
||||||
|
Mirror: true,
|
||||||
|
TrimHostname: true,
|
||||||
|
TLSConfig: tlsConfig,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// v2 registry
|
||||||
|
endpoints = append(endpoints, APIEndpoint{
|
||||||
|
URL: DEFAULT_V2_REGISTRY,
|
||||||
|
Version: APIVersion2,
|
||||||
|
Official: true,
|
||||||
|
TrimHostname: true,
|
||||||
|
TLSConfig: tlsConfig,
|
||||||
|
})
|
||||||
|
// v1 mirrors
|
||||||
|
// TODO(tiborvass): shouldn't we remove v1 mirrors from here, since v1 mirrors are kinda special?
|
||||||
|
for _, mirror := range s.Config.Mirrors {
|
||||||
|
endpoints = append(endpoints, APIEndpoint{
|
||||||
|
URL: mirror,
|
||||||
|
// guess mirrors are v1
|
||||||
|
Version: APIVersion1,
|
||||||
|
Mirror: true,
|
||||||
|
TrimHostname: true,
|
||||||
|
TLSConfig: tlsConfig,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// v1 registry
|
||||||
|
endpoints = append(endpoints, APIEndpoint{
|
||||||
|
URL: DEFAULT_V1_REGISTRY,
|
||||||
|
Version: APIVersion1,
|
||||||
|
Official: true,
|
||||||
|
TrimHostname: true,
|
||||||
|
TLSConfig: tlsConfig,
|
||||||
|
})
|
||||||
|
return endpoints, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
slashIndex := strings.IndexRune(repoName, '/')
|
||||||
|
if slashIndex <= 0 {
|
||||||
|
return nil, fmt.Errorf("invalid repo name: missing '/': %s", repoName)
|
||||||
|
}
|
||||||
|
hostname := repoName[:slashIndex]
|
||||||
|
|
||||||
|
tlsConfig, err = s.TlsConfig(hostname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
isSecure := !tlsConfig.InsecureSkipVerify
|
||||||
|
|
||||||
|
v2Versions := []auth.APIVersion{
|
||||||
|
{
|
||||||
|
Type: "registry",
|
||||||
|
Version: "2.0",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
endpoints = []APIEndpoint{
|
||||||
|
{
|
||||||
|
URL: "https://" + hostname,
|
||||||
|
Version: APIVersion2,
|
||||||
|
TrimHostname: true,
|
||||||
|
TLSConfig: tlsConfig,
|
||||||
|
VersionHeader: DEFAULT_REGISTRY_VERSION_HEADER,
|
||||||
|
Versions: v2Versions,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: "https://" + hostname,
|
||||||
|
Version: APIVersion1,
|
||||||
|
TrimHostname: true,
|
||||||
|
TLSConfig: tlsConfig,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isSecure {
|
||||||
|
endpoints = append(endpoints, APIEndpoint{
|
||||||
|
URL: "http://" + hostname,
|
||||||
|
Version: APIVersion2,
|
||||||
|
TrimHostname: true,
|
||||||
|
// used to check if supposed to be secure via InsecureSkipVerify
|
||||||
|
TLSConfig: tlsConfig,
|
||||||
|
VersionHeader: DEFAULT_REGISTRY_VERSION_HEADER,
|
||||||
|
Versions: v2Versions,
|
||||||
|
}, APIEndpoint{
|
||||||
|
URL: "http://" + hostname,
|
||||||
|
Version: APIVersion1,
|
||||||
|
TrimHostname: true,
|
||||||
|
// used to check if supposed to be secure via InsecureSkipVerify
|
||||||
|
TLSConfig: tlsConfig,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return endpoints, nil
|
||||||
|
}
|
||||||
|
|
|
@ -98,7 +98,7 @@ func (tr *authTransport) RoundTrip(orig *http.Request) (*http.Response, error) {
|
||||||
return tr.RoundTripper.RoundTrip(orig)
|
return tr.RoundTripper.RoundTrip(orig)
|
||||||
}
|
}
|
||||||
|
|
||||||
req := transport.CloneRequest(orig)
|
req := cloneRequest(orig)
|
||||||
tr.mu.Lock()
|
tr.mu.Lock()
|
||||||
tr.modReq[orig] = req
|
tr.modReq[orig] = req
|
||||||
tr.mu.Unlock()
|
tr.mu.Unlock()
|
||||||
|
@ -164,12 +164,11 @@ func NewSession(client *http.Client, authConfig *cliconfig.AuthConfig, endpoint
|
||||||
|
|
||||||
// If we're working with a standalone private registry over HTTPS, send Basic Auth headers
|
// If we're working with a standalone private registry over HTTPS, send Basic Auth headers
|
||||||
// alongside all our requests.
|
// alongside all our requests.
|
||||||
if endpoint.VersionString(1) != IndexServerAddress() && endpoint.URL.Scheme == "https" {
|
if endpoint.VersionString(1) != INDEXSERVER && endpoint.URL.Scheme == "https" {
|
||||||
info, err := endpoint.Ping()
|
info, err := endpoint.Ping()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if info.Standalone && authConfig != nil {
|
if info.Standalone && authConfig != nil {
|
||||||
logrus.Debugf("Endpoint %s is eligible for private registry. Enabling decorator.", endpoint.String())
|
logrus.Debugf("Endpoint %s is eligible for private registry. Enabling decorator.", endpoint.String())
|
||||||
alwaysSetBasicAuth = true
|
alwaysSetBasicAuth = true
|
||||||
|
@ -265,7 +264,7 @@ func (r *Session) GetRemoteImageLayer(imgID, registry string, imgSize int64) (io
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Error while getting from the server: %v", err)
|
return nil, fmt.Errorf("Error while getting from the server: %v", err)
|
||||||
}
|
}
|
||||||
// TODO: why are we doing retries at this level?
|
// TODO(tiborvass): why are we doing retries at this level?
|
||||||
// These retries should be generic to both v1 and v2
|
// These retries should be generic to both v1 and v2
|
||||||
for i := 1; i <= retries; i++ {
|
for i := 1; i <= retries; i++ {
|
||||||
statusCode = 0
|
statusCode = 0
|
||||||
|
@ -432,7 +431,7 @@ func (r *Session) GetRepositoryData(remote string) (*RepositoryData, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forge a better object from the retrieved data
|
// Forge a better object from the retrieved data
|
||||||
imgsData := make(map[string]*ImgData)
|
imgsData := make(map[string]*ImgData, len(remoteChecksums))
|
||||||
for _, elem := range remoteChecksums {
|
for _, elem := range remoteChecksums {
|
||||||
imgsData[elem.ID] = elem
|
imgsData[elem.ID] = elem
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,414 +0,0 @@
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/docker/distribution/digest"
|
|
||||||
"github.com/docker/distribution/registry/api/v2"
|
|
||||||
"github.com/docker/docker/pkg/httputils"
|
|
||||||
)
|
|
||||||
|
|
||||||
const DockerDigestHeader = "Docker-Content-Digest"
|
|
||||||
|
|
||||||
func getV2Builder(e *Endpoint) *v2.URLBuilder {
|
|
||||||
if e.URLBuilder == nil {
|
|
||||||
e.URLBuilder = v2.NewURLBuilder(e.URL)
|
|
||||||
}
|
|
||||||
return e.URLBuilder
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Session) V2RegistryEndpoint(index *IndexInfo) (ep *Endpoint, err error) {
|
|
||||||
// TODO check if should use Mirror
|
|
||||||
if index.Official {
|
|
||||||
ep, err = newEndpoint(REGISTRYSERVER, true, nil)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
err = validateEndpoint(ep)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else if r.indexEndpoint.String() == index.GetAuthConfigKey() {
|
|
||||||
ep = r.indexEndpoint
|
|
||||||
} else {
|
|
||||||
ep, err = NewEndpoint(index, nil)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ep.URLBuilder = v2.NewURLBuilder(ep.URL)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetV2Authorization gets the authorization needed to the given image
|
|
||||||
// If readonly access is requested, then the authorization may
|
|
||||||
// only be used for Get operations.
|
|
||||||
func (r *Session) GetV2Authorization(ep *Endpoint, imageName string, readOnly bool) (auth *RequestAuthorization, err error) {
|
|
||||||
scopes := []string{"pull"}
|
|
||||||
if !readOnly {
|
|
||||||
scopes = append(scopes, "push")
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("Getting authorization for %s %s", imageName, scopes)
|
|
||||||
return NewRequestAuthorization(r.GetAuthConfig(true), ep, "repository", imageName, scopes), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// 1) Check if TarSum of each layer exists /v2/
|
|
||||||
// 1.a) if 200, continue
|
|
||||||
// 1.b) if 300, then push the
|
|
||||||
// 1.c) if anything else, err
|
|
||||||
// 2) PUT the created/signed manifest
|
|
||||||
//
|
|
||||||
|
|
||||||
// GetV2ImageManifest simply fetches the bytes of a manifest and the remote
|
|
||||||
// digest, if available in the request. Note that the application shouldn't
|
|
||||||
// rely on the untrusted remoteDigest, and should also verify against a
|
|
||||||
// locally provided digest, if applicable.
|
|
||||||
func (r *Session) GetV2ImageManifest(ep *Endpoint, imageName, tagName string, auth *RequestAuthorization) (remoteDigest digest.Digest, p []byte, err error) {
|
|
||||||
routeURL, err := getV2Builder(ep).BuildManifestURL(imageName, tagName)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
method := "GET"
|
|
||||||
logrus.Debugf("[registry] Calling %q %s", method, routeURL)
|
|
||||||
|
|
||||||
req, err := http.NewRequest(method, routeURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := auth.Authorize(req); err != nil {
|
|
||||||
return "", nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := r.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
if res.StatusCode == 401 {
|
|
||||||
return "", nil, errLoginRequired
|
|
||||||
} else if res.StatusCode == 404 {
|
|
||||||
return "", nil, ErrDoesNotExist
|
|
||||||
}
|
|
||||||
return "", nil, httputils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to fetch for %s:%s", res.StatusCode, imageName, tagName), res)
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err = ioutil.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return "", nil, fmt.Errorf("Error while reading the http response: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dgstHdr := res.Header.Get(DockerDigestHeader)
|
|
||||||
if dgstHdr != "" {
|
|
||||||
remoteDigest, err = digest.ParseDigest(dgstHdr)
|
|
||||||
if err != nil {
|
|
||||||
// NOTE(stevvooe): Including the remote digest is optional. We
|
|
||||||
// don't need to verify against it, but it is good practice.
|
|
||||||
remoteDigest = ""
|
|
||||||
logrus.Debugf("error parsing remote digest when fetching %v: %v", routeURL, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// - Succeeded to head image blob (already exists)
|
|
||||||
// - Failed with no error (continue to Push the Blob)
|
|
||||||
// - Failed with error
|
|
||||||
func (r *Session) HeadV2ImageBlob(ep *Endpoint, imageName string, dgst digest.Digest, auth *RequestAuthorization) (bool, error) {
|
|
||||||
routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, dgst)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
method := "HEAD"
|
|
||||||
logrus.Debugf("[registry] Calling %q %s", method, routeURL)
|
|
||||||
|
|
||||||
req, err := http.NewRequest(method, routeURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if err := auth.Authorize(req); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
res, err := r.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
res.Body.Close() // close early, since we're not needing a body on this call .. yet?
|
|
||||||
switch {
|
|
||||||
case res.StatusCode >= 200 && res.StatusCode < 400:
|
|
||||||
// return something indicating no push needed
|
|
||||||
return true, nil
|
|
||||||
case res.StatusCode == 401:
|
|
||||||
return false, errLoginRequired
|
|
||||||
case res.StatusCode == 404:
|
|
||||||
// return something indicating blob push needed
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, httputils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying head request for %s - %s", res.StatusCode, imageName, dgst), res)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Session) GetV2ImageBlob(ep *Endpoint, imageName string, dgst digest.Digest, blobWrtr io.Writer, auth *RequestAuthorization) error {
|
|
||||||
routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, dgst)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
method := "GET"
|
|
||||||
logrus.Debugf("[registry] Calling %q %s", method, routeURL)
|
|
||||||
req, err := http.NewRequest(method, routeURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := auth.Authorize(req); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
res, err := r.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
if res.StatusCode == 401 {
|
|
||||||
return errLoginRequired
|
|
||||||
}
|
|
||||||
return httputils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to pull %s blob", res.StatusCode, imageName), res)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = io.Copy(blobWrtr, res.Body)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Session) GetV2ImageBlobReader(ep *Endpoint, imageName string, dgst digest.Digest, auth *RequestAuthorization) (io.ReadCloser, int64, error) {
|
|
||||||
routeURL, err := getV2Builder(ep).BuildBlobURL(imageName, dgst)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
method := "GET"
|
|
||||||
logrus.Debugf("[registry] Calling %q %s", method, routeURL)
|
|
||||||
req, err := http.NewRequest(method, routeURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
if err := auth.Authorize(req); err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
res, err := r.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
if res.StatusCode == 401 {
|
|
||||||
return nil, 0, errLoginRequired
|
|
||||||
}
|
|
||||||
return nil, 0, httputils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to pull %s blob - %s", res.StatusCode, imageName, dgst), res)
|
|
||||||
}
|
|
||||||
lenStr := res.Header.Get("Content-Length")
|
|
||||||
l, err := strconv.ParseInt(lenStr, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.Body, l, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push the image to the server for storage.
|
|
||||||
// 'layer' is an uncompressed reader of the blob to be pushed.
|
|
||||||
// The server will generate it's own checksum calculation.
|
|
||||||
func (r *Session) PutV2ImageBlob(ep *Endpoint, imageName string, dgst digest.Digest, blobRdr io.Reader, auth *RequestAuthorization) error {
|
|
||||||
location, err := r.initiateBlobUpload(ep, imageName, auth)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
method := "PUT"
|
|
||||||
logrus.Debugf("[registry] Calling %q %s", method, location)
|
|
||||||
req, err := http.NewRequest(method, location, ioutil.NopCloser(blobRdr))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
queryParams := req.URL.Query()
|
|
||||||
queryParams.Add("digest", dgst.String())
|
|
||||||
req.URL.RawQuery = queryParams.Encode()
|
|
||||||
if err := auth.Authorize(req); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
res, err := r.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
if res.StatusCode != 201 {
|
|
||||||
if res.StatusCode == 401 {
|
|
||||||
return errLoginRequired
|
|
||||||
}
|
|
||||||
errBody, err := ioutil.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
logrus.Debugf("Unexpected response from server: %q %#v", errBody, res.Header)
|
|
||||||
return httputils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to push %s blob - %s", res.StatusCode, imageName, dgst), res)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// initiateBlobUpload gets the blob upload location for the given image name.
|
|
||||||
func (r *Session) initiateBlobUpload(ep *Endpoint, imageName string, auth *RequestAuthorization) (location string, err error) {
|
|
||||||
routeURL, err := getV2Builder(ep).BuildBlobUploadURL(imageName)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("[registry] Calling %q %s", "POST", routeURL)
|
|
||||||
req, err := http.NewRequest("POST", routeURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := auth.Authorize(req); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
res, err := r.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.StatusCode != http.StatusAccepted {
|
|
||||||
if res.StatusCode == http.StatusUnauthorized {
|
|
||||||
return "", errLoginRequired
|
|
||||||
}
|
|
||||||
if res.StatusCode == http.StatusNotFound {
|
|
||||||
return "", ErrDoesNotExist
|
|
||||||
}
|
|
||||||
|
|
||||||
errBody, err := ioutil.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("Unexpected response from server: %q %#v", errBody, res.Header)
|
|
||||||
return "", httputils.NewHTTPRequestError(fmt.Sprintf("Server error: unexpected %d response status trying to initiate upload of %s", res.StatusCode, imageName), res)
|
|
||||||
}
|
|
||||||
|
|
||||||
if location = res.Header.Get("Location"); location == "" {
|
|
||||||
return "", fmt.Errorf("registry did not return a Location header for resumable blob upload for image %s", imageName)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally Push the (signed) manifest of the blobs we've just pushed
|
|
||||||
func (r *Session) PutV2ImageManifest(ep *Endpoint, imageName, tagName string, signedManifest, rawManifest []byte, auth *RequestAuthorization) (digest.Digest, error) {
|
|
||||||
routeURL, err := getV2Builder(ep).BuildManifestURL(imageName, tagName)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
method := "PUT"
|
|
||||||
logrus.Debugf("[registry] Calling %q %s", method, routeURL)
|
|
||||||
req, err := http.NewRequest(method, routeURL, bytes.NewReader(signedManifest))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if err := auth.Authorize(req); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
res, err := r.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
// All 2xx and 3xx responses can be accepted for a put.
|
|
||||||
if res.StatusCode >= 400 {
|
|
||||||
if res.StatusCode == 401 {
|
|
||||||
return "", errLoginRequired
|
|
||||||
}
|
|
||||||
errBody, err := ioutil.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
logrus.Debugf("Unexpected response from server: %q %#v", errBody, res.Header)
|
|
||||||
return "", httputils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to push %s:%s manifest", res.StatusCode, imageName, tagName), res)
|
|
||||||
}
|
|
||||||
|
|
||||||
hdrDigest, err := digest.ParseDigest(res.Header.Get(DockerDigestHeader))
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("invalid manifest digest from registry: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dgstVerifier, err := digest.NewDigestVerifier(hdrDigest)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("invalid manifest digest from registry: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dgstVerifier.Write(rawManifest)
|
|
||||||
|
|
||||||
if !dgstVerifier.Verified() {
|
|
||||||
computedDigest, _ := digest.FromBytes(rawManifest)
|
|
||||||
return "", fmt.Errorf("unable to verify manifest digest: registry has %q, computed %q", hdrDigest, computedDigest)
|
|
||||||
}
|
|
||||||
|
|
||||||
return hdrDigest, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type remoteTags struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Tags []string `json:"tags"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Given a repository name, returns a json array of string tags
|
|
||||||
func (r *Session) GetV2RemoteTags(ep *Endpoint, imageName string, auth *RequestAuthorization) ([]string, error) {
|
|
||||||
routeURL, err := getV2Builder(ep).BuildTagsURL(imageName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
method := "GET"
|
|
||||||
logrus.Debugf("[registry] Calling %q %s", method, routeURL)
|
|
||||||
|
|
||||||
req, err := http.NewRequest(method, routeURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := auth.Authorize(req); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
res, err := r.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
if res.StatusCode == 401 {
|
|
||||||
return nil, errLoginRequired
|
|
||||||
} else if res.StatusCode == 404 {
|
|
||||||
return nil, ErrDoesNotExist
|
|
||||||
}
|
|
||||||
return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to fetch for %s", res.StatusCode, imageName), res)
|
|
||||||
}
|
|
||||||
|
|
||||||
var remote remoteTags
|
|
||||||
if err := json.NewDecoder(res.Body).Decode(&remote); err != nil {
|
|
||||||
return nil, fmt.Errorf("Error while decoding the http response: %s", err)
|
|
||||||
}
|
|
||||||
return remote.Tags, nil
|
|
||||||
}
|
|
Loading…
Reference in a new issue