commit
						96272e1c9a
					
				| 
						 | 
					@ -2,7 +2,6 @@ package registry
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import (
 | 
					import (
 | 
				
			||||||
	"encoding/json"
 | 
						"encoding/json"
 | 
				
			||||||
	"errors"
 | 
					 | 
				
			||||||
	"fmt"
 | 
						"fmt"
 | 
				
			||||||
	"io/ioutil"
 | 
						"io/ioutil"
 | 
				
			||||||
	"net/http"
 | 
						"net/http"
 | 
				
			||||||
| 
						 | 
					@ -34,9 +33,9 @@ func scanForApiVersion(hostname string) (string, APIVersion) {
 | 
				
			||||||
	return hostname, DefaultAPIVersion
 | 
						return hostname, DefaultAPIVersion
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func NewEndpoint(hostname string) (*Endpoint, error) {
 | 
					func NewEndpoint(hostname string, secure bool) (*Endpoint, error) {
 | 
				
			||||||
	var (
 | 
						var (
 | 
				
			||||||
		endpoint        Endpoint
 | 
							endpoint        = Endpoint{secure: secure}
 | 
				
			||||||
		trimmedHostname string
 | 
							trimmedHostname string
 | 
				
			||||||
		err             error
 | 
							err             error
 | 
				
			||||||
	)
 | 
						)
 | 
				
			||||||
| 
						 | 
					@ -49,14 +48,27 @@ func NewEndpoint(hostname string) (*Endpoint, error) {
 | 
				
			||||||
		return nil, err
 | 
							return nil, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Try HTTPS ping to registry
 | 
				
			||||||
	endpoint.URL.Scheme = "https"
 | 
						endpoint.URL.Scheme = "https"
 | 
				
			||||||
	if _, err := endpoint.Ping(); err != nil {
 | 
						if _, err := endpoint.Ping(); err != nil {
 | 
				
			||||||
		log.Debugf("Registry %s does not work (%s), falling back to http", endpoint, err)
 | 
					
 | 
				
			||||||
		// TODO: Check if http fallback is enabled
 | 
							//TODO: triggering highland build can be done there without "failing"
 | 
				
			||||||
		endpoint.URL.Scheme = "http"
 | 
					
 | 
				
			||||||
		if _, err = endpoint.Ping(); err != nil {
 | 
							if secure {
 | 
				
			||||||
			return nil, errors.New("Invalid Registry endpoint: " + err.Error())
 | 
								// 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 nil, 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)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// If registry is insecure and HTTPS failed, fallback to HTTP.
 | 
				
			||||||
 | 
							log.Debugf("Error from registry %q marked as insecure: %v. Insecurely falling back to HTTP", endpoint, err)
 | 
				
			||||||
 | 
							endpoint.URL.Scheme = "http"
 | 
				
			||||||
 | 
							_, err2 := endpoint.Ping()
 | 
				
			||||||
 | 
							if err2 == nil {
 | 
				
			||||||
 | 
								return &endpoint, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							return nil, fmt.Errorf("Invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return &endpoint, nil
 | 
						return &endpoint, nil
 | 
				
			||||||
| 
						 | 
					@ -65,6 +77,7 @@ func NewEndpoint(hostname string) (*Endpoint, error) {
 | 
				
			||||||
type Endpoint struct {
 | 
					type Endpoint struct {
 | 
				
			||||||
	URL     *url.URL
 | 
						URL     *url.URL
 | 
				
			||||||
	Version APIVersion
 | 
						Version APIVersion
 | 
				
			||||||
 | 
						secure  bool
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Get the formated URL for the root of this registry Endpoint
 | 
					// Get the formated URL for the root of this registry Endpoint
 | 
				
			||||||
| 
						 | 
					@ -88,7 +101,7 @@ func (e Endpoint) Ping() (RegistryInfo, error) {
 | 
				
			||||||
		return RegistryInfo{Standalone: false}, err
 | 
							return RegistryInfo{Standalone: false}, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	resp, _, err := doRequest(req, nil, ConnectTimeout)
 | 
						resp, _, err := doRequest(req, nil, ConnectTimeout, e.secure)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return RegistryInfo{Standalone: false}, err
 | 
							return RegistryInfo{Standalone: false}, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -127,3 +140,19 @@ func (e Endpoint) Ping() (RegistryInfo, error) {
 | 
				
			||||||
	log.Debugf("RegistryInfo.Standalone: %t", info.Standalone)
 | 
						log.Debugf("RegistryInfo.Standalone: %t", info.Standalone)
 | 
				
			||||||
	return info, nil
 | 
						return info, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// IsSecure returns false if the provided hostname is part of the list of insecure registries.
 | 
				
			||||||
 | 
					// Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs.
 | 
				
			||||||
 | 
					func IsSecure(hostname string, insecureRegistries []string) bool {
 | 
				
			||||||
 | 
						if hostname == IndexServerAddress() {
 | 
				
			||||||
 | 
							return true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for _, h := range insecureRegistries {
 | 
				
			||||||
 | 
							if hostname == h {
 | 
				
			||||||
 | 
								return false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return true
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -14,6 +14,7 @@ import (
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						"github.com/docker/docker/pkg/log"
 | 
				
			||||||
	"github.com/docker/docker/utils"
 | 
						"github.com/docker/docker/utils"
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -35,13 +36,21 @@ const (
 | 
				
			||||||
	ConnectTimeout
 | 
						ConnectTimeout
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func newClient(jar http.CookieJar, roots *x509.CertPool, cert *tls.Certificate, timeout TimeoutType) *http.Client {
 | 
					func newClient(jar http.CookieJar, roots *x509.CertPool, cert *tls.Certificate, timeout TimeoutType, secure bool) *http.Client {
 | 
				
			||||||
	tlsConfig := tls.Config{RootCAs: roots}
 | 
						tlsConfig := tls.Config{
 | 
				
			||||||
 | 
							RootCAs: roots,
 | 
				
			||||||
 | 
							// Avoid fallback to SSL protocols < TLS1.0
 | 
				
			||||||
 | 
							MinVersion: tls.VersionTLS10,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if cert != nil {
 | 
						if cert != nil {
 | 
				
			||||||
		tlsConfig.Certificates = append(tlsConfig.Certificates, *cert)
 | 
							tlsConfig.Certificates = append(tlsConfig.Certificates, *cert)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if !secure {
 | 
				
			||||||
 | 
							tlsConfig.InsecureSkipVerify = true
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	httpTransport := &http.Transport{
 | 
						httpTransport := &http.Transport{
 | 
				
			||||||
		DisableKeepAlives: true,
 | 
							DisableKeepAlives: true,
 | 
				
			||||||
		Proxy:             http.ProxyFromEnvironment,
 | 
							Proxy:             http.ProxyFromEnvironment,
 | 
				
			||||||
| 
						 | 
					@ -78,7 +87,13 @@ func newClient(jar http.CookieJar, roots *x509.CertPool, cert *tls.Certificate,
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func doRequest(req *http.Request, jar http.CookieJar, timeout TimeoutType) (*http.Response, *http.Client, error) {
 | 
					func doRequest(req *http.Request, jar http.CookieJar, timeout TimeoutType, secure bool) (*http.Response, *http.Client, error) {
 | 
				
			||||||
 | 
						var (
 | 
				
			||||||
 | 
							pool  *x509.CertPool
 | 
				
			||||||
 | 
							certs []*tls.Certificate
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if secure && req.URL.Scheme == "https" {
 | 
				
			||||||
		hasFile := func(files []os.FileInfo, name string) bool {
 | 
							hasFile := func(files []os.FileInfo, name string) bool {
 | 
				
			||||||
			for _, f := range files {
 | 
								for _, f := range files {
 | 
				
			||||||
				if f.Name() == name {
 | 
									if f.Name() == name {
 | 
				
			||||||
| 
						 | 
					@ -89,21 +104,18 @@ func doRequest(req *http.Request, jar http.CookieJar, timeout TimeoutType) (*htt
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		hostDir := path.Join("/etc/docker/certs.d", req.URL.Host)
 | 
							hostDir := path.Join("/etc/docker/certs.d", req.URL.Host)
 | 
				
			||||||
 | 
							log.Debugf("hostDir: %s", hostDir)
 | 
				
			||||||
		fs, err := ioutil.ReadDir(hostDir)
 | 
							fs, err := ioutil.ReadDir(hostDir)
 | 
				
			||||||
		if err != nil && !os.IsNotExist(err) {
 | 
							if err != nil && !os.IsNotExist(err) {
 | 
				
			||||||
			return nil, nil, err
 | 
								return nil, nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	var (
 | 
					 | 
				
			||||||
		pool  *x509.CertPool
 | 
					 | 
				
			||||||
		certs []*tls.Certificate
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		for _, f := range fs {
 | 
							for _, f := range fs {
 | 
				
			||||||
			if strings.HasSuffix(f.Name(), ".crt") {
 | 
								if strings.HasSuffix(f.Name(), ".crt") {
 | 
				
			||||||
				if pool == nil {
 | 
									if pool == nil {
 | 
				
			||||||
					pool = x509.NewCertPool()
 | 
										pool = x509.NewCertPool()
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
 | 
									log.Debugf("crt: %s", hostDir+"/"+f.Name())
 | 
				
			||||||
				data, err := ioutil.ReadFile(path.Join(hostDir, f.Name()))
 | 
									data, err := ioutil.ReadFile(path.Join(hostDir, f.Name()))
 | 
				
			||||||
				if err != nil {
 | 
									if err != nil {
 | 
				
			||||||
					return nil, nil, err
 | 
										return nil, nil, err
 | 
				
			||||||
| 
						 | 
					@ -113,6 +125,7 @@ func doRequest(req *http.Request, jar http.CookieJar, timeout TimeoutType) (*htt
 | 
				
			||||||
			if strings.HasSuffix(f.Name(), ".cert") {
 | 
								if strings.HasSuffix(f.Name(), ".cert") {
 | 
				
			||||||
				certName := f.Name()
 | 
									certName := f.Name()
 | 
				
			||||||
				keyName := certName[:len(certName)-5] + ".key"
 | 
									keyName := certName[:len(certName)-5] + ".key"
 | 
				
			||||||
 | 
									log.Debugf("cert: %s", hostDir+"/"+f.Name())
 | 
				
			||||||
				if !hasFile(fs, keyName) {
 | 
									if !hasFile(fs, keyName) {
 | 
				
			||||||
					return nil, nil, fmt.Errorf("Missing key %s for certificate %s", keyName, certName)
 | 
										return nil, nil, fmt.Errorf("Missing key %s for certificate %s", keyName, certName)
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
| 
						 | 
					@ -125,22 +138,25 @@ func doRequest(req *http.Request, jar http.CookieJar, timeout TimeoutType) (*htt
 | 
				
			||||||
			if strings.HasSuffix(f.Name(), ".key") {
 | 
								if strings.HasSuffix(f.Name(), ".key") {
 | 
				
			||||||
				keyName := f.Name()
 | 
									keyName := f.Name()
 | 
				
			||||||
				certName := keyName[:len(keyName)-4] + ".cert"
 | 
									certName := keyName[:len(keyName)-4] + ".cert"
 | 
				
			||||||
 | 
									log.Debugf("key: %s", hostDir+"/"+f.Name())
 | 
				
			||||||
				if !hasFile(fs, certName) {
 | 
									if !hasFile(fs, certName) {
 | 
				
			||||||
					return nil, nil, fmt.Errorf("Missing certificate %s for key %s", certName, keyName)
 | 
										return nil, nil, fmt.Errorf("Missing certificate %s for key %s", certName, keyName)
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if len(certs) == 0 {
 | 
						if len(certs) == 0 {
 | 
				
			||||||
		client := newClient(jar, pool, nil, timeout)
 | 
							client := newClient(jar, pool, nil, timeout, secure)
 | 
				
			||||||
		res, err := client.Do(req)
 | 
							res, err := client.Do(req)
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return nil, nil, err
 | 
								return nil, nil, err
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return res, client, nil
 | 
							return res, client, nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	for i, cert := range certs {
 | 
						for i, cert := range certs {
 | 
				
			||||||
		client := newClient(jar, pool, cert, timeout)
 | 
							client := newClient(jar, pool, cert, timeout, secure)
 | 
				
			||||||
		res, err := client.Do(req)
 | 
							res, err := client.Do(req)
 | 
				
			||||||
		// If this is the last cert, otherwise, continue to next cert if 403 or 5xx
 | 
							// If this is the last cert, otherwise, continue to next cert if 403 or 5xx
 | 
				
			||||||
		if i == len(certs)-1 || err == nil && res.StatusCode != 403 && res.StatusCode < 500 {
 | 
							if i == len(certs)-1 || err == nil && res.StatusCode != 403 && res.StatusCode < 500 {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,7 +18,7 @@ var (
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func spawnTestRegistrySession(t *testing.T) *Session {
 | 
					func spawnTestRegistrySession(t *testing.T) *Session {
 | 
				
			||||||
	authConfig := &AuthConfig{}
 | 
						authConfig := &AuthConfig{}
 | 
				
			||||||
	endpoint, err := NewEndpoint(makeURL("/v1/"))
 | 
						endpoint, err := NewEndpoint(makeURL("/v1/"), false)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		t.Fatal(err)
 | 
							t.Fatal(err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -30,7 +30,7 @@ func spawnTestRegistrySession(t *testing.T) *Session {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func TestPingRegistryEndpoint(t *testing.T) {
 | 
					func TestPingRegistryEndpoint(t *testing.T) {
 | 
				
			||||||
	ep, err := NewEndpoint(makeURL("/v1/"))
 | 
						ep, err := NewEndpoint(makeURL("/v1/"), false)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		t.Fatal(err)
 | 
							t.Fatal(err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,12 +13,15 @@ import (
 | 
				
			||||||
//  'pull': Download images from any registry (TODO)
 | 
					//  'pull': Download images from any registry (TODO)
 | 
				
			||||||
//  'push': Upload images to any registry (TODO)
 | 
					//  'push': Upload images to any registry (TODO)
 | 
				
			||||||
type Service struct {
 | 
					type Service struct {
 | 
				
			||||||
 | 
						insecureRegistries []string
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// NewService returns a new instance of Service ready to be
 | 
					// NewService returns a new instance of Service ready to be
 | 
				
			||||||
// installed no an engine.
 | 
					// installed no an engine.
 | 
				
			||||||
func NewService() *Service {
 | 
					func NewService(insecureRegistries []string) *Service {
 | 
				
			||||||
	return &Service{}
 | 
						return &Service{
 | 
				
			||||||
 | 
							insecureRegistries: insecureRegistries,
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Install installs registry capabilities to eng.
 | 
					// Install installs registry capabilities to eng.
 | 
				
			||||||
| 
						 | 
					@ -32,15 +35,12 @@ func (s *Service) Install(eng *engine.Engine) error {
 | 
				
			||||||
// and returns OK if authentication was sucessful.
 | 
					// and returns OK if authentication was sucessful.
 | 
				
			||||||
// It can be used to verify the validity of a client's credentials.
 | 
					// It can be used to verify the validity of a client's credentials.
 | 
				
			||||||
func (s *Service) Auth(job *engine.Job) engine.Status {
 | 
					func (s *Service) Auth(job *engine.Job) engine.Status {
 | 
				
			||||||
	var (
 | 
						var authConfig = new(AuthConfig)
 | 
				
			||||||
		err        error
 | 
					 | 
				
			||||||
		authConfig = &AuthConfig{}
 | 
					 | 
				
			||||||
	)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	job.GetenvJson("authConfig", authConfig)
 | 
						job.GetenvJson("authConfig", authConfig)
 | 
				
			||||||
	// TODO: this is only done here because auth and registry need to be merged into one pkg
 | 
					
 | 
				
			||||||
	if addr := authConfig.ServerAddress; addr != "" && addr != IndexServerAddress() {
 | 
						if addr := authConfig.ServerAddress; addr != "" && addr != IndexServerAddress() {
 | 
				
			||||||
		endpoint, err := NewEndpoint(addr)
 | 
							endpoint, err := NewEndpoint(addr, IsSecure(addr, s.insecureRegistries))
 | 
				
			||||||
		if err != nil {
 | 
							if err != nil {
 | 
				
			||||||
			return job.Error(err)
 | 
								return job.Error(err)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					@ -49,11 +49,13 @@ func (s *Service) Auth(job *engine.Job) engine.Status {
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		authConfig.ServerAddress = endpoint.String()
 | 
							authConfig.ServerAddress = endpoint.String()
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	status, err := Login(authConfig, HTTPRequestFactory(nil))
 | 
						status, err := Login(authConfig, HTTPRequestFactory(nil))
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return job.Error(err)
 | 
							return job.Error(err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	job.Printf("%s\n", status)
 | 
						job.Printf("%s\n", status)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return engine.StatusOK
 | 
						return engine.StatusOK
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -89,7 +91,10 @@ func (s *Service) Search(job *engine.Job) engine.Status {
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return job.Error(err)
 | 
							return job.Error(err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	endpoint, err := NewEndpoint(hostname)
 | 
					
 | 
				
			||||||
 | 
						secure := IsSecure(hostname, s.insecureRegistries)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						endpoint, err := NewEndpoint(hostname, secure)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return job.Error(err)
 | 
							return job.Error(err)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -64,7 +64,7 @@ func NewSession(authConfig *AuthConfig, factory *utils.HTTPRequestFactory, endpo
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (r *Session) doRequest(req *http.Request) (*http.Response, *http.Client, error) {
 | 
					func (r *Session) doRequest(req *http.Request) (*http.Response, *http.Client, error) {
 | 
				
			||||||
	return doRequest(req, r.jar, r.timeout)
 | 
						return doRequest(req, r.jar, r.timeout, r.indexEndpoint.secure)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Retrieve the history of a given image from the Registry.
 | 
					// Retrieve the history of a given image from the Registry.
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue