Convert TOML to YAML, tweaks to work with Jekyll
This commit is contained in:
parent
1f6cc853d3
commit
0fb207c822
63 changed files with 413 additions and 4307 deletions
|
@ -1,3 +1,7 @@
|
||||||
|
---
|
||||||
|
{}
|
||||||
|
---
|
||||||
|
|
||||||
FROM docs/base:oss
|
FROM docs/base:oss
|
||||||
MAINTAINER Docker Docs <docs@docker.com>
|
MAINTAINER Docker Docs <docs@docker.com>
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
---
|
||||||
|
{}
|
||||||
|
---
|
||||||
|
|
||||||
.PHONY: all default docs docs-build docs-shell shell test
|
.PHONY: all default docs docs-build docs-shell shell test
|
||||||
|
|
||||||
# to allow `make DOCSDIR=docs docs-shell` (to create a bind mount in docs)
|
# to allow `make DOCSDIR=docs docs-shell` (to create a bind mount in docs)
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
draft: true
|
||||||
draft = true
|
---
|
||||||
+++
|
|
||||||
<![end-metadata]-->
|
|
||||||
|
|
||||||
# Architecture
|
# Architecture
|
||||||
|
|
||||||
|
|
300
docs/auth.go
300
docs/auth.go
|
@ -1,300 +0,0 @@
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/docker/distribution/registry/client/auth"
|
|
||||||
"github.com/docker/distribution/registry/client/transport"
|
|
||||||
"github.com/docker/engine-api/types"
|
|
||||||
registrytypes "github.com/docker/engine-api/types/registry"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// AuthClientID is used the ClientID used for the token server
|
|
||||||
AuthClientID = "docker"
|
|
||||||
)
|
|
||||||
|
|
||||||
// loginV1 tries to register/login to the v1 registry server.
|
|
||||||
func loginV1(authConfig *types.AuthConfig, apiEndpoint APIEndpoint, userAgent string) (string, string, error) {
|
|
||||||
registryEndpoint, err := apiEndpoint.ToV1Endpoint(userAgent, nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
serverAddress := registryEndpoint.String()
|
|
||||||
|
|
||||||
logrus.Debugf("attempting v1 login to registry endpoint %s", serverAddress)
|
|
||||||
|
|
||||||
if serverAddress == "" {
|
|
||||||
return "", "", fmt.Errorf("Server Error: Server Address not set.")
|
|
||||||
}
|
|
||||||
|
|
||||||
loginAgainstOfficialIndex := serverAddress == IndexServer
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", serverAddress+"users/", nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
req.SetBasicAuth(authConfig.Username, authConfig.Password)
|
|
||||||
resp, err := registryEndpoint.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
// fallback when request could not be completed
|
|
||||||
return "", "", fallbackError{
|
|
||||||
err: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
if resp.StatusCode == http.StatusOK {
|
|
||||||
return "Login Succeeded", "", nil
|
|
||||||
} else if resp.StatusCode == http.StatusUnauthorized {
|
|
||||||
if loginAgainstOfficialIndex {
|
|
||||||
return "", "", fmt.Errorf("Wrong login/password, please try again. Haven't got a Docker ID? Create one at https://hub.docker.com")
|
|
||||||
}
|
|
||||||
return "", "", fmt.Errorf("Wrong login/password, please try again")
|
|
||||||
} else if resp.StatusCode == http.StatusForbidden {
|
|
||||||
if loginAgainstOfficialIndex {
|
|
||||||
return "", "", fmt.Errorf("Login: Account is not active. Please check your e-mail for a confirmation link.")
|
|
||||||
}
|
|
||||||
// *TODO: Use registry configuration to determine what this says, if anything?
|
|
||||||
return "", "", fmt.Errorf("Login: Account is not active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress)
|
|
||||||
} else if resp.StatusCode == http.StatusInternalServerError { // Issue #14326
|
|
||||||
logrus.Errorf("%s returned status code %d. Response Body :\n%s", req.URL.String(), resp.StatusCode, body)
|
|
||||||
return "", "", fmt.Errorf("Internal Server Error")
|
|
||||||
}
|
|
||||||
return "", "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body,
|
|
||||||
resp.StatusCode, resp.Header)
|
|
||||||
}
|
|
||||||
|
|
||||||
type loginCredentialStore struct {
|
|
||||||
authConfig *types.AuthConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lcs loginCredentialStore) Basic(*url.URL) (string, string) {
|
|
||||||
return lcs.authConfig.Username, lcs.authConfig.Password
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lcs loginCredentialStore) RefreshToken(*url.URL, string) string {
|
|
||||||
return lcs.authConfig.IdentityToken
|
|
||||||
}
|
|
||||||
|
|
||||||
func (lcs loginCredentialStore) SetRefreshToken(u *url.URL, service, token string) {
|
|
||||||
lcs.authConfig.IdentityToken = token
|
|
||||||
}
|
|
||||||
|
|
||||||
type staticCredentialStore struct {
|
|
||||||
auth *types.AuthConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewStaticCredentialStore returns a credential store
|
|
||||||
// which always returns the same credential values.
|
|
||||||
func NewStaticCredentialStore(auth *types.AuthConfig) auth.CredentialStore {
|
|
||||||
return staticCredentialStore{
|
|
||||||
auth: auth,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (scs staticCredentialStore) Basic(*url.URL) (string, string) {
|
|
||||||
if scs.auth == nil {
|
|
||||||
return "", ""
|
|
||||||
}
|
|
||||||
return scs.auth.Username, scs.auth.Password
|
|
||||||
}
|
|
||||||
|
|
||||||
func (scs staticCredentialStore) RefreshToken(*url.URL, string) string {
|
|
||||||
if scs.auth == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return scs.auth.IdentityToken
|
|
||||||
}
|
|
||||||
|
|
||||||
func (scs staticCredentialStore) SetRefreshToken(*url.URL, string, string) {
|
|
||||||
}
|
|
||||||
|
|
||||||
type fallbackError struct {
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err fallbackError) Error() string {
|
|
||||||
return err.err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
// loginV2 tries to login to the v2 registry server. The given registry
|
|
||||||
// endpoint will be pinged to get authorization challenges. These challenges
|
|
||||||
// will be used to authenticate against the registry to validate credentials.
|
|
||||||
func loginV2(authConfig *types.AuthConfig, endpoint APIEndpoint, userAgent string) (string, string, error) {
|
|
||||||
logrus.Debugf("attempting v2 login to registry endpoint %s", strings.TrimRight(endpoint.URL.String(), "/")+"/v2/")
|
|
||||||
|
|
||||||
modifiers := DockerHeaders(userAgent, nil)
|
|
||||||
authTransport := transport.NewTransport(NewTransport(endpoint.TLSConfig), modifiers...)
|
|
||||||
|
|
||||||
credentialAuthConfig := *authConfig
|
|
||||||
creds := loginCredentialStore{
|
|
||||||
authConfig: &credentialAuthConfig,
|
|
||||||
}
|
|
||||||
|
|
||||||
loginClient, foundV2, err := v2AuthHTTPClient(endpoint.URL, authTransport, modifiers, creds, nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
endpointStr := strings.TrimRight(endpoint.URL.String(), "/") + "/v2/"
|
|
||||||
req, err := http.NewRequest("GET", endpointStr, nil)
|
|
||||||
if err != nil {
|
|
||||||
if !foundV2 {
|
|
||||||
err = fallbackError{err: err}
|
|
||||||
}
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := loginClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
if !foundV2 {
|
|
||||||
err = fallbackError{err: err}
|
|
||||||
}
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
// TODO(dmcgowan): Attempt to further interpret result, status code and error code string
|
|
||||||
err := fmt.Errorf("login attempt to %s failed with status: %d %s", endpointStr, resp.StatusCode, http.StatusText(resp.StatusCode))
|
|
||||||
if !foundV2 {
|
|
||||||
err = fallbackError{err: err}
|
|
||||||
}
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return "Login Succeeded", credentialAuthConfig.IdentityToken, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifiers []transport.RequestModifier, creds auth.CredentialStore, scopes []auth.Scope) (*http.Client, bool, error) {
|
|
||||||
challengeManager, foundV2, err := PingV2Registry(endpoint, authTransport)
|
|
||||||
if err != nil {
|
|
||||||
if !foundV2 {
|
|
||||||
err = fallbackError{err: err}
|
|
||||||
}
|
|
||||||
return nil, foundV2, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tokenHandlerOptions := auth.TokenHandlerOptions{
|
|
||||||
Transport: authTransport,
|
|
||||||
Credentials: creds,
|
|
||||||
OfflineAccess: true,
|
|
||||||
ClientID: AuthClientID,
|
|
||||||
Scopes: scopes,
|
|
||||||
}
|
|
||||||
tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions)
|
|
||||||
basicHandler := auth.NewBasicHandler(creds)
|
|
||||||
modifiers = append(modifiers, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
|
|
||||||
tr := transport.NewTransport(authTransport, modifiers...)
|
|
||||||
|
|
||||||
return &http.Client{
|
|
||||||
Transport: tr,
|
|
||||||
Timeout: 15 * time.Second,
|
|
||||||
}, foundV2, nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResolveAuthConfig matches an auth configuration to a server address or a URL
|
|
||||||
func ResolveAuthConfig(authConfigs map[string]types.AuthConfig, index *registrytypes.IndexInfo) types.AuthConfig {
|
|
||||||
configKey := GetAuthConfigKey(index)
|
|
||||||
// First try the happy case
|
|
||||||
if c, found := authConfigs[configKey]; found || index.Official {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
convertToHostname := func(url string) string {
|
|
||||||
stripped := url
|
|
||||||
if strings.HasPrefix(url, "http://") {
|
|
||||||
stripped = strings.Replace(url, "http://", "", 1)
|
|
||||||
} else if strings.HasPrefix(url, "https://") {
|
|
||||||
stripped = strings.Replace(url, "https://", "", 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
nameParts := strings.SplitN(stripped, "/", 2)
|
|
||||||
|
|
||||||
return nameParts[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Maybe they have a legacy config file, we will iterate the keys converting
|
|
||||||
// them to the new format and testing
|
|
||||||
for registry, ac := range authConfigs {
|
|
||||||
if configKey == convertToHostname(registry) {
|
|
||||||
return ac
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// When all else fails, return an empty auth config
|
|
||||||
return types.AuthConfig{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PingResponseError is used when the response from a ping
|
|
||||||
// was received but invalid.
|
|
||||||
type PingResponseError struct {
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err PingResponseError) Error() string {
|
|
||||||
return err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
// PingV2Registry attempts to ping a v2 registry and on success return a
|
|
||||||
// challenge manager for the supported authentication types and
|
|
||||||
// whether v2 was confirmed by the response. If a response is received but
|
|
||||||
// cannot be interpreted a PingResponseError will be returned.
|
|
||||||
func PingV2Registry(endpoint *url.URL, transport http.RoundTripper) (auth.ChallengeManager, bool, error) {
|
|
||||||
var (
|
|
||||||
foundV2 = false
|
|
||||||
v2Version = auth.APIVersion{
|
|
||||||
Type: "registry",
|
|
||||||
Version: "2.0",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
pingClient := &http.Client{
|
|
||||||
Transport: transport,
|
|
||||||
Timeout: 15 * time.Second,
|
|
||||||
}
|
|
||||||
endpointStr := strings.TrimRight(endpoint.String(), "/") + "/v2/"
|
|
||||||
req, err := http.NewRequest("GET", endpointStr, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
resp, err := pingClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
versions := auth.APIVersions(resp, DefaultRegistryVersionHeader)
|
|
||||||
for _, pingVersion := range versions {
|
|
||||||
if pingVersion == v2Version {
|
|
||||||
// The version header indicates we're definitely
|
|
||||||
// talking to a v2 registry. So don't allow future
|
|
||||||
// fallbacks to the v1 protocol.
|
|
||||||
|
|
||||||
foundV2 = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
challengeManager := auth.NewSimpleChallengeManager()
|
|
||||||
if err := challengeManager.AddResponse(resp); err != nil {
|
|
||||||
return nil, foundV2, PingResponseError{
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return challengeManager, foundV2, nil
|
|
||||||
}
|
|
|
@ -1,120 +0,0 @@
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/docker/engine-api/types"
|
|
||||||
registrytypes "github.com/docker/engine-api/types/registry"
|
|
||||||
)
|
|
||||||
|
|
||||||
func buildAuthConfigs() map[string]types.AuthConfig {
|
|
||||||
authConfigs := map[string]types.AuthConfig{}
|
|
||||||
|
|
||||||
for _, registry := range []string{"testIndex", IndexServer} {
|
|
||||||
authConfigs[registry] = types.AuthConfig{
|
|
||||||
Username: "docker-user",
|
|
||||||
Password: "docker-pass",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return authConfigs
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSameAuthDataPostSave(t *testing.T) {
|
|
||||||
authConfigs := buildAuthConfigs()
|
|
||||||
authConfig := authConfigs["testIndex"]
|
|
||||||
if authConfig.Username != "docker-user" {
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
if authConfig.Password != "docker-pass" {
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
if authConfig.Auth != "" {
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolveAuthConfigIndexServer(t *testing.T) {
|
|
||||||
authConfigs := buildAuthConfigs()
|
|
||||||
indexConfig := authConfigs[IndexServer]
|
|
||||||
|
|
||||||
officialIndex := ®istrytypes.IndexInfo{
|
|
||||||
Official: true,
|
|
||||||
}
|
|
||||||
privateIndex := ®istrytypes.IndexInfo{
|
|
||||||
Official: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
resolved := ResolveAuthConfig(authConfigs, officialIndex)
|
|
||||||
assertEqual(t, resolved, indexConfig, "Expected ResolveAuthConfig to return IndexServer")
|
|
||||||
|
|
||||||
resolved = ResolveAuthConfig(authConfigs, privateIndex)
|
|
||||||
assertNotEqual(t, resolved, indexConfig, "Expected ResolveAuthConfig to not return IndexServer")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestResolveAuthConfigFullURL(t *testing.T) {
|
|
||||||
authConfigs := buildAuthConfigs()
|
|
||||||
|
|
||||||
registryAuth := types.AuthConfig{
|
|
||||||
Username: "foo-user",
|
|
||||||
Password: "foo-pass",
|
|
||||||
}
|
|
||||||
localAuth := types.AuthConfig{
|
|
||||||
Username: "bar-user",
|
|
||||||
Password: "bar-pass",
|
|
||||||
}
|
|
||||||
officialAuth := types.AuthConfig{
|
|
||||||
Username: "baz-user",
|
|
||||||
Password: "baz-pass",
|
|
||||||
}
|
|
||||||
authConfigs[IndexServer] = officialAuth
|
|
||||||
|
|
||||||
expectedAuths := map[string]types.AuthConfig{
|
|
||||||
"registry.example.com": registryAuth,
|
|
||||||
"localhost:8000": localAuth,
|
|
||||||
"registry.com": localAuth,
|
|
||||||
}
|
|
||||||
|
|
||||||
validRegistries := map[string][]string{
|
|
||||||
"registry.example.com": {
|
|
||||||
"https://registry.example.com/v1/",
|
|
||||||
"http://registry.example.com/v1/",
|
|
||||||
"registry.example.com",
|
|
||||||
"registry.example.com/v1/",
|
|
||||||
},
|
|
||||||
"localhost:8000": {
|
|
||||||
"https://localhost:8000/v1/",
|
|
||||||
"http://localhost:8000/v1/",
|
|
||||||
"localhost:8000",
|
|
||||||
"localhost:8000/v1/",
|
|
||||||
},
|
|
||||||
"registry.com": {
|
|
||||||
"https://registry.com/v1/",
|
|
||||||
"http://registry.com/v1/",
|
|
||||||
"registry.com",
|
|
||||||
"registry.com/v1/",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for configKey, registries := range validRegistries {
|
|
||||||
configured, ok := expectedAuths[configKey]
|
|
||||||
if !ok {
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
index := ®istrytypes.IndexInfo{
|
|
||||||
Name: configKey,
|
|
||||||
}
|
|
||||||
for _, registry := range registries {
|
|
||||||
authConfigs[registry] = configured
|
|
||||||
resolved := ResolveAuthConfig(authConfigs, index)
|
|
||||||
if resolved.Username != configured.Username || resolved.Password != configured.Password {
|
|
||||||
t.Errorf("%s -> %v != %v\n", registry, resolved, configured)
|
|
||||||
}
|
|
||||||
delete(authConfigs, registry)
|
|
||||||
resolved = ResolveAuthConfig(authConfigs, index)
|
|
||||||
if resolved.Username == configured.Username || resolved.Password == configured.Password {
|
|
||||||
t.Errorf("%s -> %v == %v\n", registry, resolved, configured)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +1,13 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: describes get by digest pitfall
|
||||||
title = "Compatibility"
|
keywords:
|
||||||
description = "describes get by digest pitfall"
|
- registry, manifest, images, tags, repository, distribution, digest
|
||||||
keywords = ["registry, manifest, images, tags, repository, distribution, digest"]
|
menu:
|
||||||
[menu.main]
|
main:
|
||||||
parent="smn_registry_ref"
|
parent: smn_registry_ref
|
||||||
weight=9
|
weight: 9
|
||||||
+++
|
title: Compatibility
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
# Registry Compatibility
|
# Registry Compatibility
|
||||||
|
|
||||||
|
|
274
docs/config.go
274
docs/config.go
|
@ -1,274 +0,0 @@
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/docker/docker/opts"
|
|
||||||
flag "github.com/docker/docker/pkg/mflag"
|
|
||||||
"github.com/docker/docker/reference"
|
|
||||||
registrytypes "github.com/docker/engine-api/types/registry"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ServiceOptions holds command line options.
|
|
||||||
type ServiceOptions struct {
|
|
||||||
Mirrors []string `json:"registry-mirrors,omitempty"`
|
|
||||||
InsecureRegistries []string `json:"insecure-registries,omitempty"`
|
|
||||||
|
|
||||||
// V2Only controls access to legacy registries. If it is set to true via the
|
|
||||||
// command line flag the daemon will not attempt to contact v1 legacy registries
|
|
||||||
V2Only bool `json:"disable-legacy-registry,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// serviceConfig holds daemon configuration for the registry service.
|
|
||||||
type serviceConfig struct {
|
|
||||||
registrytypes.ServiceConfig
|
|
||||||
V2Only bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
// DefaultNamespace is the default namespace
|
|
||||||
DefaultNamespace = "docker.io"
|
|
||||||
// DefaultRegistryVersionHeader is the name of the default HTTP header
|
|
||||||
// that carries Registry version info
|
|
||||||
DefaultRegistryVersionHeader = "Docker-Distribution-Api-Version"
|
|
||||||
|
|
||||||
// IndexServer is the v1 registry server used for user auth + account creation
|
|
||||||
IndexServer = DefaultV1Registry.String() + "/v1/"
|
|
||||||
// IndexName is the name of the index
|
|
||||||
IndexName = "docker.io"
|
|
||||||
|
|
||||||
// NotaryServer is the endpoint serving the Notary trust server
|
|
||||||
NotaryServer = "https://notary.docker.io"
|
|
||||||
|
|
||||||
// DefaultV1Registry is the URI of the default v1 registry
|
|
||||||
DefaultV1Registry = &url.URL{
|
|
||||||
Scheme: "https",
|
|
||||||
Host: "index.docker.io",
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultV2Registry is the URI of the default v2 registry
|
|
||||||
DefaultV2Registry = &url.URL{
|
|
||||||
Scheme: "https",
|
|
||||||
Host: "registry-1.docker.io",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrInvalidRepositoryName is an error returned if the repository name did
|
|
||||||
// not have the correct form
|
|
||||||
ErrInvalidRepositoryName = errors.New("Invalid repository name (ex: \"registry.domain.tld/myrepos\")")
|
|
||||||
|
|
||||||
emptyServiceConfig = newServiceConfig(ServiceOptions{})
|
|
||||||
)
|
|
||||||
|
|
||||||
// for mocking in unit tests
|
|
||||||
var lookupIP = net.LookupIP
|
|
||||||
|
|
||||||
// InstallCliFlags adds command-line options to the top-level flag parser for
|
|
||||||
// the current process.
|
|
||||||
func (options *ServiceOptions) InstallCliFlags(cmd *flag.FlagSet, usageFn func(string) string) {
|
|
||||||
mirrors := opts.NewNamedListOptsRef("registry-mirrors", &options.Mirrors, ValidateMirror)
|
|
||||||
cmd.Var(mirrors, []string{"-registry-mirror"}, usageFn("Preferred Docker registry mirror"))
|
|
||||||
|
|
||||||
insecureRegistries := opts.NewNamedListOptsRef("insecure-registries", &options.InsecureRegistries, ValidateIndexName)
|
|
||||||
cmd.Var(insecureRegistries, []string{"-insecure-registry"}, usageFn("Enable insecure registry communication"))
|
|
||||||
|
|
||||||
cmd.BoolVar(&options.V2Only, []string{"-disable-legacy-registry"}, false, usageFn("Disable contacting legacy registries"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// newServiceConfig returns a new instance of ServiceConfig
|
|
||||||
func newServiceConfig(options ServiceOptions) *serviceConfig {
|
|
||||||
// Localhost is by default considered as an insecure registry
|
|
||||||
// This is a stop-gap for people who are running a private registry on localhost (especially on Boot2docker).
|
|
||||||
//
|
|
||||||
// TODO: should we deprecate this once it is easier for people to set up a TLS registry or change
|
|
||||||
// daemon flags on boot2docker?
|
|
||||||
options.InsecureRegistries = append(options.InsecureRegistries, "127.0.0.0/8")
|
|
||||||
|
|
||||||
config := &serviceConfig{
|
|
||||||
ServiceConfig: registrytypes.ServiceConfig{
|
|
||||||
InsecureRegistryCIDRs: make([]*registrytypes.NetIPNet, 0),
|
|
||||||
IndexConfigs: make(map[string]*registrytypes.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,
|
|
||||||
},
|
|
||||||
V2Only: options.V2Only,
|
|
||||||
}
|
|
||||||
// Split --insecure-registry into CIDR and registry-specific settings.
|
|
||||||
for _, r := range options.InsecureRegistries {
|
|
||||||
// Check if CIDR was passed to --insecure-registry
|
|
||||||
_, ipnet, err := net.ParseCIDR(r)
|
|
||||||
if err == nil {
|
|
||||||
// Valid CIDR.
|
|
||||||
config.InsecureRegistryCIDRs = append(config.InsecureRegistryCIDRs, (*registrytypes.NetIPNet)(ipnet))
|
|
||||||
} else {
|
|
||||||
// Assume `host:port` if not CIDR.
|
|
||||||
config.IndexConfigs[r] = ®istrytypes.IndexInfo{
|
|
||||||
Name: r,
|
|
||||||
Mirrors: make([]string, 0),
|
|
||||||
Secure: false,
|
|
||||||
Official: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure public registry.
|
|
||||||
config.IndexConfigs[IndexName] = ®istrytypes.IndexInfo{
|
|
||||||
Name: IndexName,
|
|
||||||
Mirrors: config.Mirrors,
|
|
||||||
Secure: true,
|
|
||||||
Official: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
return config
|
|
||||||
}
|
|
||||||
|
|
||||||
// isSecureIndex returns false if the provided indexName is part of the list of insecure registries
|
|
||||||
// Insecure registries accept HTTP and/or accept HTTPS with certificates from unknown CAs.
|
|
||||||
//
|
|
||||||
// The list of insecure registries can contain an element with CIDR notation to specify a whole subnet.
|
|
||||||
// If the subnet contains one of the IPs of the registry specified by indexName, the latter is considered
|
|
||||||
// insecure.
|
|
||||||
//
|
|
||||||
// indexName should be a URL.Host (`host:port` or `host`) where the `host` part can be either a domain name
|
|
||||||
// or an IP address. If it is a domain name, then it will be resolved in order to check if the IP is contained
|
|
||||||
// in a subnet. If the resolving is not successful, isSecureIndex will only try to match hostname to any element
|
|
||||||
// of insecureRegistries.
|
|
||||||
func isSecureIndex(config *serviceConfig, indexName string) bool {
|
|
||||||
// Check for configured index, first. This is needed in case isSecureIndex
|
|
||||||
// is called from anything besides newIndexInfo, in order to honor per-index configurations.
|
|
||||||
if index, ok := config.IndexConfigs[indexName]; ok {
|
|
||||||
return index.Secure
|
|
||||||
}
|
|
||||||
|
|
||||||
host, _, err := net.SplitHostPort(indexName)
|
|
||||||
if err != nil {
|
|
||||||
// assume indexName is of the form `host` without the port and go on.
|
|
||||||
host = indexName
|
|
||||||
}
|
|
||||||
|
|
||||||
addrs, err := lookupIP(host)
|
|
||||||
if err != nil {
|
|
||||||
ip := net.ParseIP(host)
|
|
||||||
if ip != nil {
|
|
||||||
addrs = []net.IP{ip}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if ip == nil, then `host` is neither an IP nor it could be looked up,
|
|
||||||
// either because the index is unreachable, or because the index is behind an HTTP proxy.
|
|
||||||
// So, len(addrs) == 0 and we're not aborting.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try CIDR notation only if addrs has any elements, i.e. if `host`'s IP could be determined.
|
|
||||||
for _, addr := range addrs {
|
|
||||||
for _, ipnet := range config.InsecureRegistryCIDRs {
|
|
||||||
// check if the addr falls in the subnet
|
|
||||||
if (*net.IPNet)(ipnet).Contains(addr) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateMirror validates an HTTP(S) registry mirror
|
|
||||||
func ValidateMirror(val string) (string, error) {
|
|
||||||
uri, err := url.Parse(val)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("%s is not a valid URI", val)
|
|
||||||
}
|
|
||||||
|
|
||||||
if uri.Scheme != "http" && uri.Scheme != "https" {
|
|
||||||
return "", fmt.Errorf("Unsupported scheme %s", uri.Scheme)
|
|
||||||
}
|
|
||||||
|
|
||||||
if uri.Path != "" || uri.RawQuery != "" || uri.Fragment != "" {
|
|
||||||
return "", fmt.Errorf("Unsupported path/query/fragment at end of the URI")
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s://%s/", uri.Scheme, uri.Host), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateIndexName validates an index name.
|
|
||||||
func ValidateIndexName(val string) (string, error) {
|
|
||||||
if val == reference.LegacyDefaultHostname {
|
|
||||||
val = reference.DefaultHostname
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(val, "-") || strings.HasSuffix(val, "-") {
|
|
||||||
return "", fmt.Errorf("Invalid index name (%s). Cannot begin or end with a hyphen.", val)
|
|
||||||
}
|
|
||||||
return val, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateNoScheme(reposName string) error {
|
|
||||||
if strings.Contains(reposName, "://") {
|
|
||||||
// It cannot contain a scheme!
|
|
||||||
return ErrInvalidRepositoryName
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// newIndexInfo returns IndexInfo configuration from indexName
|
|
||||||
func newIndexInfo(config *serviceConfig, indexName string) (*registrytypes.IndexInfo, error) {
|
|
||||||
var err error
|
|
||||||
indexName, err = ValidateIndexName(indexName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return any configured index info, first.
|
|
||||||
if index, ok := config.IndexConfigs[indexName]; ok {
|
|
||||||
return index, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct a non-configured index info.
|
|
||||||
index := ®istrytypes.IndexInfo{
|
|
||||||
Name: indexName,
|
|
||||||
Mirrors: make([]string, 0),
|
|
||||||
Official: false,
|
|
||||||
}
|
|
||||||
index.Secure = isSecureIndex(config, indexName)
|
|
||||||
return index, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAuthConfigKey special-cases using the full index address of the official
|
|
||||||
// index as the AuthConfig key, and uses the (host)name[:port] for private indexes.
|
|
||||||
func GetAuthConfigKey(index *registrytypes.IndexInfo) string {
|
|
||||||
if index.Official {
|
|
||||||
return IndexServer
|
|
||||||
}
|
|
||||||
return index.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
// newRepositoryInfo validates and breaks down a repository name into a RepositoryInfo
|
|
||||||
func newRepositoryInfo(config *serviceConfig, name reference.Named) (*RepositoryInfo, error) {
|
|
||||||
index, err := newIndexInfo(config, name.Hostname())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
official := !strings.ContainsRune(name.Name(), '/')
|
|
||||||
return &RepositoryInfo{name, index, official}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseRepositoryInfo performs the breakdown of a repository name into a RepositoryInfo, but
|
|
||||||
// lacks registry configuration.
|
|
||||||
func ParseRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) {
|
|
||||||
return newRepositoryInfo(emptyServiceConfig, reposName)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseSearchIndexInfo will use repository name to get back an indexInfo.
|
|
||||||
func ParseSearchIndexInfo(reposName string) (*registrytypes.IndexInfo, error) {
|
|
||||||
indexName, _ := splitReposSearchTerm(reposName)
|
|
||||||
|
|
||||||
indexInfo, err := newIndexInfo(emptyServiceConfig, indexName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return indexInfo, nil
|
|
||||||
}
|
|
|
@ -1,49 +0,0 @@
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestValidateMirror(t *testing.T) {
|
|
||||||
valid := []string{
|
|
||||||
"http://mirror-1.com",
|
|
||||||
"https://mirror-1.com",
|
|
||||||
"http://localhost",
|
|
||||||
"https://localhost",
|
|
||||||
"http://localhost:5000",
|
|
||||||
"https://localhost:5000",
|
|
||||||
"http://127.0.0.1",
|
|
||||||
"https://127.0.0.1",
|
|
||||||
"http://127.0.0.1:5000",
|
|
||||||
"https://127.0.0.1:5000",
|
|
||||||
}
|
|
||||||
|
|
||||||
invalid := []string{
|
|
||||||
"!invalid!://%as%",
|
|
||||||
"ftp://mirror-1.com",
|
|
||||||
"http://mirror-1.com/",
|
|
||||||
"http://mirror-1.com/?q=foo",
|
|
||||||
"http://mirror-1.com/v1/",
|
|
||||||
"http://mirror-1.com/v1/?q=foo",
|
|
||||||
"http://mirror-1.com/v1/?q=foo#frag",
|
|
||||||
"http://mirror-1.com?q=foo",
|
|
||||||
"https://mirror-1.com#frag",
|
|
||||||
"https://mirror-1.com/",
|
|
||||||
"https://mirror-1.com/#frag",
|
|
||||||
"https://mirror-1.com/v1/",
|
|
||||||
"https://mirror-1.com/v1/#",
|
|
||||||
"https://mirror-1.com?q",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, address := range valid {
|
|
||||||
if ret, err := ValidateMirror(address); err != nil || ret == "" {
|
|
||||||
t.Errorf("ValidateMirror(`"+address+"`) got %s %s", ret, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, address := range invalid {
|
|
||||||
if ret, err := ValidateMirror(address); err == nil || ret != "" {
|
|
||||||
t.Errorf("ValidateMirror(`"+address+"`) got %s %s", ret, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,16 +0,0 @@
|
||||||
// +build !windows
|
|
||||||
|
|
||||||
package registry
|
|
||||||
|
|
||||||
var (
|
|
||||||
// CertsDir is the directory where certificates are stored
|
|
||||||
CertsDir = "/etc/docker/certs.d"
|
|
||||||
)
|
|
||||||
|
|
||||||
// cleanPath is used to ensure that a directory name is valid on the target
|
|
||||||
// platform. It will be passed in something *similar* to a URL such as
|
|
||||||
// https:/index.docker.io/v1. Not all platforms support directory names
|
|
||||||
// which contain those characters (such as : on Windows)
|
|
||||||
func cleanPath(s string) string {
|
|
||||||
return s
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// CertsDir is the directory where certificates are stored
|
|
||||||
var CertsDir = os.Getenv("programdata") + `\docker\certs.d`
|
|
||||||
|
|
||||||
// cleanPath is used to ensure that a directory name is valid on the target
|
|
||||||
// platform. It will be passed in something *similar* to a URL such as
|
|
||||||
// https:\index.docker.io\v1. Not all platforms support directory names
|
|
||||||
// which contain those characters (such as : on Windows)
|
|
||||||
func cleanPath(s string) string {
|
|
||||||
return filepath.FromSlash(strings.Replace(s, ":", "", -1))
|
|
||||||
}
|
|
|
@ -1,13 +1,13 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Explains how to configure a registry
|
||||||
title = "Configuring a registry"
|
keywords:
|
||||||
description = "Explains how to configure a registry"
|
- registry, on-prem, images, tags, repository, distribution, configuration
|
||||||
keywords = ["registry, on-prem, images, tags, repository, distribution, configuration"]
|
menu:
|
||||||
[menu.main]
|
main:
|
||||||
parent="smn_registry"
|
parent: smn_registry
|
||||||
weight=4
|
weight: 4
|
||||||
+++
|
title: Configuring a registry
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
# Registry Configuration Reference
|
# Registry Configuration Reference
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Explains how to deploy a registry
|
||||||
title = "Deploying a registry server"
|
keywords:
|
||||||
description = "Explains how to deploy a registry"
|
- registry, on-prem, images, tags, repository, distribution, deployment
|
||||||
keywords = ["registry, on-prem, images, tags, repository, distribution, deployment"]
|
menu:
|
||||||
[menu.main]
|
main:
|
||||||
parent="smn_registry"
|
parent: smn_registry
|
||||||
weight=3
|
weight: 3
|
||||||
+++
|
title: Deploying a registry server
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
# Deploying a registry server
|
# Deploying a registry server
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: describes deprecated functionality
|
||||||
title = "Deprecated Features"
|
keywords:
|
||||||
description = "describes deprecated functionality"
|
- registry, manifest, images, signatures, repository, distribution, digest
|
||||||
keywords = ["registry, manifest, images, signatures, repository, distribution, digest"]
|
menu:
|
||||||
[menu.main]
|
main:
|
||||||
parent="smn_registry_ref"
|
parent: smn_registry_ref
|
||||||
weight=8
|
weight: 8
|
||||||
+++
|
title: Deprecated Features
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
# Docker Registry Deprecation
|
# Docker Registry Deprecation
|
||||||
|
|
||||||
|
|
|
@ -1,78 +0,0 @@
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestEndpointParse(t *testing.T) {
|
|
||||||
testData := []struct {
|
|
||||||
str string
|
|
||||||
expected string
|
|
||||||
}{
|
|
||||||
{IndexServer, IndexServer},
|
|
||||||
{"http://0.0.0.0:5000/v1/", "http://0.0.0.0:5000/v1/"},
|
|
||||||
{"http://0.0.0.0:5000", "http://0.0.0.0:5000/v1/"},
|
|
||||||
{"0.0.0.0:5000", "https://0.0.0.0:5000/v1/"},
|
|
||||||
{"http://0.0.0.0:5000/nonversion/", "http://0.0.0.0:5000/nonversion/v1/"},
|
|
||||||
{"http://0.0.0.0:5000/v0/", "http://0.0.0.0:5000/v0/v1/"},
|
|
||||||
}
|
|
||||||
for _, td := range testData {
|
|
||||||
e, err := newV1EndpointFromStr(td.str, nil, "", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("%q: %s", td.str, err)
|
|
||||||
}
|
|
||||||
if e == nil {
|
|
||||||
t.Logf("something's fishy, endpoint for %q is nil", td.str)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if e.String() != td.expected {
|
|
||||||
t.Errorf("expected %q, got %q", td.expected, e.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEndpointParseInvalid(t *testing.T) {
|
|
||||||
testData := []string{
|
|
||||||
"http://0.0.0.0:5000/v2/",
|
|
||||||
}
|
|
||||||
for _, td := range testData {
|
|
||||||
e, err := newV1EndpointFromStr(td, nil, "", nil)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("expected error parsing %q: parsed as %q", td, e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that a registry endpoint that responds with a 401 only is determined
|
|
||||||
// to be a valid v1 registry endpoint
|
|
||||||
func TestValidateEndpoint(t *testing.T) {
|
|
||||||
requireBasicAuthHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Add("WWW-Authenticate", `Basic realm="localhost"`)
|
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Make a test server which should validate as a v1 server.
|
|
||||||
testServer := httptest.NewServer(requireBasicAuthHandler)
|
|
||||||
defer testServer.Close()
|
|
||||||
|
|
||||||
testServerURL, err := url.Parse(testServer.URL)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testEndpoint := V1Endpoint{
|
|
||||||
URL: testServerURL,
|
|
||||||
client: HTTPClient(NewTransport(nil)),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = validateEndpoint(&testEndpoint); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if testEndpoint.URL.Scheme != "http" {
|
|
||||||
t.Fatalf("expecting to validate endpoint as http, got url %s", testEndpoint.String())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,198 +0,0 @@
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/docker/distribution/registry/client/transport"
|
|
||||||
registrytypes "github.com/docker/engine-api/types/registry"
|
|
||||||
)
|
|
||||||
|
|
||||||
// V1Endpoint stores basic information about a V1 registry endpoint.
|
|
||||||
type V1Endpoint struct {
|
|
||||||
client *http.Client
|
|
||||||
URL *url.URL
|
|
||||||
IsSecure bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewV1Endpoint parses the given address to return a registry endpoint.
|
|
||||||
func NewV1Endpoint(index *registrytypes.IndexInfo, userAgent string, metaHeaders http.Header) (*V1Endpoint, error) {
|
|
||||||
tlsConfig, err := newTLSConfig(index.Name, index.Secure)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoint, err := newV1EndpointFromStr(GetAuthConfigKey(index), tlsConfig, userAgent, metaHeaders)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := validateEndpoint(endpoint); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return endpoint, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateEndpoint(endpoint *V1Endpoint) error {
|
|
||||||
logrus.Debugf("pinging registry endpoint %s", endpoint)
|
|
||||||
|
|
||||||
// Try HTTPS ping to registry
|
|
||||||
endpoint.URL.Scheme = "https"
|
|
||||||
if _, err := endpoint.Ping(); err != nil {
|
|
||||||
if endpoint.IsSecure {
|
|
||||||
// If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry`
|
|
||||||
// in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fallback to HTTP.
|
|
||||||
return fmt.Errorf("invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %s` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If registry is insecure and HTTPS failed, fallback to HTTP.
|
|
||||||
logrus.Debugf("Error from registry %q marked as insecure: %v. Insecurely falling back to HTTP", endpoint, err)
|
|
||||||
endpoint.URL.Scheme = "http"
|
|
||||||
|
|
||||||
var err2 error
|
|
||||||
if _, err2 = endpoint.Ping(); err2 == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newV1Endpoint(address url.URL, tlsConfig *tls.Config, userAgent string, metaHeaders http.Header) (*V1Endpoint, error) {
|
|
||||||
endpoint := &V1Endpoint{
|
|
||||||
IsSecure: (tlsConfig == nil || !tlsConfig.InsecureSkipVerify),
|
|
||||||
URL: new(url.URL),
|
|
||||||
}
|
|
||||||
|
|
||||||
*endpoint.URL = address
|
|
||||||
|
|
||||||
// TODO(tiborvass): make sure a ConnectTimeout transport is used
|
|
||||||
tr := NewTransport(tlsConfig)
|
|
||||||
endpoint.client = HTTPClient(transport.NewTransport(tr, DockerHeaders(userAgent, metaHeaders)...))
|
|
||||||
return endpoint, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// trimV1Address trims the version off the address and returns the
|
|
||||||
// trimmed address or an error if there is a non-V1 version.
|
|
||||||
func trimV1Address(address string) (string, error) {
|
|
||||||
var (
|
|
||||||
chunks []string
|
|
||||||
apiVersionStr string
|
|
||||||
)
|
|
||||||
|
|
||||||
if strings.HasSuffix(address, "/") {
|
|
||||||
address = address[:len(address)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
chunks = strings.Split(address, "/")
|
|
||||||
apiVersionStr = chunks[len(chunks)-1]
|
|
||||||
if apiVersionStr == "v1" {
|
|
||||||
return strings.Join(chunks[:len(chunks)-1], "/"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range apiVersions {
|
|
||||||
if k != APIVersion1 && apiVersionStr == v {
|
|
||||||
return "", fmt.Errorf("unsupported V1 version path %s", apiVersionStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return address, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newV1EndpointFromStr(address string, tlsConfig *tls.Config, userAgent string, metaHeaders http.Header) (*V1Endpoint, error) {
|
|
||||||
if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
|
|
||||||
address = "https://" + address
|
|
||||||
}
|
|
||||||
|
|
||||||
address, err := trimV1Address(address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
uri, err := url.Parse(address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoint, err := newV1Endpoint(*uri, tlsConfig, userAgent, metaHeaders)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return endpoint, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the formatted URL for the root of this registry Endpoint
|
|
||||||
func (e *V1Endpoint) String() string {
|
|
||||||
return e.URL.String() + "/v1/"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Path returns a formatted string for the URL
|
|
||||||
// of this endpoint with the given path appended.
|
|
||||||
func (e *V1Endpoint) Path(path string) string {
|
|
||||||
return e.URL.String() + "/v1/" + path
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ping returns a PingResult which indicates whether the registry is standalone or not.
|
|
||||||
func (e *V1Endpoint) Ping() (PingResult, error) {
|
|
||||||
logrus.Debugf("attempting v1 ping for registry endpoint %s", e)
|
|
||||||
|
|
||||||
if e.String() == IndexServer {
|
|
||||||
// Skip the check, we know this one is valid
|
|
||||||
// (and we never want to fallback to http in case of error)
|
|
||||||
return PingResult{Standalone: false}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", e.Path("_ping"), nil)
|
|
||||||
if err != nil {
|
|
||||||
return PingResult{Standalone: false}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := e.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return PingResult{Standalone: false}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
jsonString, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return PingResult{Standalone: false}, fmt.Errorf("error while reading the http response: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the header is absent, we assume true for compatibility with earlier
|
|
||||||
// versions of the registry. default to true
|
|
||||||
info := PingResult{
|
|
||||||
Standalone: true,
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(jsonString, &info); err != nil {
|
|
||||||
logrus.Debugf("Error unmarshalling the _ping PingResult: %s", err)
|
|
||||||
// don't stop here. Just assume sane defaults
|
|
||||||
}
|
|
||||||
if hdr := resp.Header.Get("X-Docker-Registry-Version"); hdr != "" {
|
|
||||||
logrus.Debugf("Registry version header: '%s'", hdr)
|
|
||||||
info.Version = hdr
|
|
||||||
}
|
|
||||||
logrus.Debugf("PingResult.Version: %q", info.Version)
|
|
||||||
|
|
||||||
standalone := resp.Header.Get("X-Docker-Registry-Standalone")
|
|
||||||
logrus.Debugf("Registry standalone header: '%s'", standalone)
|
|
||||||
// Accepted values are "true" (case-insensitive) and "1".
|
|
||||||
if strings.EqualFold(standalone, "true") || standalone == "1" {
|
|
||||||
info.Standalone = true
|
|
||||||
} else if len(standalone) > 0 {
|
|
||||||
// there is a header set, and it is not "true" or "1", so assume fails
|
|
||||||
info.Standalone = false
|
|
||||||
}
|
|
||||||
logrus.Debugf("PingResult.Standalone: %t", info.Standalone)
|
|
||||||
return info, nil
|
|
||||||
}
|
|
|
@ -1,13 +1,13 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: High level discussion of garbage collection
|
||||||
title = "Garbage Collection"
|
keywords:
|
||||||
description = "High level discussion of garbage collection"
|
- registry, garbage, images, tags, repository, distribution
|
||||||
keywords = ["registry, garbage, images, tags, repository, distribution"]
|
menu:
|
||||||
[menu.main]
|
main:
|
||||||
parent="smn_registry_ref"
|
parent: smn_registry_ref
|
||||||
weight=4
|
weight: 4
|
||||||
+++
|
title: Garbage Collection
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
# Garbage Collection
|
# Garbage Collection
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
draft: true
|
||||||
draft = true
|
---
|
||||||
+++
|
|
||||||
<![end-metadata]-->
|
|
||||||
|
|
||||||
# Glossary
|
# Glossary
|
||||||
|
|
||||||
|
|
20
docs/help.md
20
docs/help.md
|
@ -1,13 +1,13 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Getting help with the Registry
|
||||||
title = "Getting help"
|
keywords:
|
||||||
description = "Getting help with the Registry"
|
- registry, on-prem, images, tags, repository, distribution, help, 101, TL;DR
|
||||||
keywords = ["registry, on-prem, images, tags, repository, distribution, help, 101, TL;DR"]
|
menu:
|
||||||
[menu.main]
|
main:
|
||||||
parent="smn_registry"
|
parent: smn_registry
|
||||||
weight=9
|
weight: 9
|
||||||
+++
|
title: Getting help
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
# Getting help
|
# Getting help
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
aliases:
|
||||||
title = "Registry Overview"
|
- /registry/overview/
|
||||||
description = "High-level overview of the Registry"
|
description: High-level overview of the Registry
|
||||||
keywords = ["registry, on-prem, images, tags, repository, distribution"]
|
keywords:
|
||||||
aliases = ["/registry/overview/"]
|
- registry, on-prem, images, tags, repository, distribution
|
||||||
[menu.main]
|
menu:
|
||||||
parent="smn_registry"
|
main:
|
||||||
weight=1
|
parent: smn_registry
|
||||||
+++
|
weight: 1
|
||||||
<![end-metadata]-->
|
title: Registry Overview
|
||||||
|
---
|
||||||
|
|
||||||
# Docker Registry
|
# Docker Registry
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Deploying a Registry in an insecure fashion
|
||||||
title = "Testing an insecure registry"
|
keywords:
|
||||||
description = "Deploying a Registry in an insecure fashion"
|
- registry, on-prem, images, tags, repository, distribution, insecure
|
||||||
keywords = ["registry, on-prem, images, tags, repository, distribution, insecure"]
|
menu:
|
||||||
[menu.main]
|
main:
|
||||||
parent="smn_registry_ref"
|
parent: smn_registry_ref
|
||||||
weight=5
|
weight: 5
|
||||||
+++
|
title: Testing an insecure registry
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
# Insecure Registry
|
# Insecure Registry
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Explains what the Registry is, basic use cases and requirements
|
||||||
title = "Understanding the Registry"
|
keywords:
|
||||||
description = "Explains what the Registry is, basic use cases and requirements"
|
- registry, on-prem, images, tags, repository, distribution, use cases, requirements
|
||||||
keywords = ["registry, on-prem, images, tags, repository, distribution, use cases, requirements"]
|
menu:
|
||||||
[menu.main]
|
main:
|
||||||
parent="smn_registry"
|
parent: smn_registry
|
||||||
weight=2
|
weight: 2
|
||||||
+++
|
title: Understanding the Registry
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
# Understanding the Registry
|
# Understanding the Registry
|
||||||
|
|
||||||
|
|
22
docs/menu.md
22
docs/menu.md
|
@ -1,14 +1,14 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: High-level overview of the Registry
|
||||||
title = "Docker Registry"
|
keywords:
|
||||||
description = "High-level overview of the Registry"
|
- registry, on-prem, images, tags, repository, distribution
|
||||||
keywords = ["registry, on-prem, images, tags, repository, distribution"]
|
menu:
|
||||||
type = "menu"
|
main:
|
||||||
[menu.main]
|
identifier: smn_registry
|
||||||
identifier="smn_registry"
|
parent: mn_components
|
||||||
parent="mn_components"
|
title: Docker Registry
|
||||||
+++
|
type: menu
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
# Overview of Docker Registry Documentation
|
# Overview of Docker Registry Documentation
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
draft: true
|
||||||
draft = true
|
---
|
||||||
+++
|
|
||||||
<![end-metadata]-->
|
|
||||||
|
|
||||||
# Migrating a 1.0 registry to 2.0
|
# Migrating a 1.0 registry to 2.0
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Explains how to work with registry notifications
|
||||||
title = "Working with notifications"
|
keywords:
|
||||||
description = "Explains how to work with registry notifications"
|
- registry, on-prem, images, tags, repository, distribution, notifications, advanced
|
||||||
keywords = ["registry, on-prem, images, tags, repository, distribution, notifications, advanced"]
|
menu:
|
||||||
[menu.main]
|
main:
|
||||||
parent="smn_registry"
|
parent: smn_registry
|
||||||
weight=5
|
weight: 5
|
||||||
+++
|
title: Working with notifications
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
# Notifications
|
# Notifications
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Restricting access to your registry using an apache proxy
|
||||||
title = "Authenticating proxy with apache"
|
keywords:
|
||||||
description = "Restricting access to your registry using an apache proxy"
|
- registry, on-prem, images, tags, repository, distribution, authentication, proxy,
|
||||||
keywords = ["registry, on-prem, images, tags, repository, distribution, authentication, proxy, apache, httpd, TLS, recipe, advanced"]
|
apache, httpd, TLS, recipe, advanced
|
||||||
[menu.main]
|
menu:
|
||||||
parent="smn_recipes"
|
main:
|
||||||
+++
|
parent: smn_recipes
|
||||||
<![end-metadata]-->
|
title: Authenticating proxy with apache
|
||||||
|
---
|
||||||
|
|
||||||
# Authenticating proxy with apache
|
# Authenticating proxy with apache
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Fun stuff to do with your registry
|
||||||
title = "Recipes Overview"
|
keywords:
|
||||||
description = "Fun stuff to do with your registry"
|
- registry, on-prem, images, tags, repository, distribution, recipes, advanced
|
||||||
keywords = ["registry, on-prem, images, tags, repository, distribution, recipes, advanced"]
|
menu:
|
||||||
[menu.main]
|
main:
|
||||||
parent="smn_recipes"
|
parent: smn_recipes
|
||||||
weight=-10
|
weight: -10
|
||||||
+++
|
title: Recipes Overview
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
# Recipes
|
# Recipes
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Registry Recipes
|
||||||
title = "Recipes"
|
keywords:
|
||||||
description = "Registry Recipes"
|
- registry, on-prem, images, tags, repository, distribution
|
||||||
keywords = ["registry, on-prem, images, tags, repository, distribution"]
|
menu:
|
||||||
type = "menu"
|
main:
|
||||||
[menu.main]
|
identifier: smn_recipes
|
||||||
identifier="smn_recipes"
|
parent: smn_registry
|
||||||
parent="smn_registry"
|
weight: 6
|
||||||
weight=6
|
title: Recipes
|
||||||
+++
|
type: menu
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
# Recipes
|
# Recipes
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Setting-up a local mirror for Docker Hub images
|
||||||
title = "Mirroring Docker Hub"
|
keywords:
|
||||||
description = "Setting-up a local mirror for Docker Hub images"
|
- registry, on-prem, images, tags, repository, distribution, mirror, Hub, recipe,
|
||||||
keywords = ["registry, on-prem, images, tags, repository, distribution, mirror, Hub, recipe, advanced"]
|
advanced
|
||||||
[menu.main]
|
menu:
|
||||||
parent="smn_recipes"
|
main:
|
||||||
+++
|
parent: smn_recipes
|
||||||
<![end-metadata]-->
|
title: Mirroring Docker Hub
|
||||||
|
---
|
||||||
|
|
||||||
# Registry as a pull through cache
|
# Registry as a pull through cache
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Restricting access to your registry using a nginx proxy
|
||||||
title = "Authenticating proxy with nginx"
|
keywords:
|
||||||
description = "Restricting access to your registry using a nginx proxy"
|
- registry, on-prem, images, tags, repository, distribution, nginx, proxy, authentication,
|
||||||
keywords = ["registry, on-prem, images, tags, repository, distribution, nginx, proxy, authentication, TLS, recipe, advanced"]
|
TLS, recipe, advanced
|
||||||
[menu.main]
|
menu:
|
||||||
parent="smn_recipes"
|
main:
|
||||||
+++
|
parent: smn_recipes
|
||||||
<![end-metadata]-->
|
title: Authenticating proxy with nginx
|
||||||
|
---
|
||||||
|
|
||||||
# Authenticating proxy with nginx
|
# Authenticating proxy with nginx
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Explains how to run a registry on OS X
|
||||||
title = "Running on OS X"
|
keywords:
|
||||||
description = "Explains how to run a registry on OS X"
|
- registry, on-prem, images, tags, repository, distribution, OS X, recipe, advanced
|
||||||
keywords = ["registry, on-prem, images, tags, repository, distribution, OS X, recipe, advanced"]
|
menu:
|
||||||
[menu.main]
|
main:
|
||||||
parent="smn_recipes"
|
parent: smn_recipes
|
||||||
+++
|
title: Running on OS X
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
# OS X Setup Guide
|
# OS X Setup Guide
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
---
|
||||||
|
{}
|
||||||
|
---
|
||||||
|
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
---
|
||||||
|
{}
|
||||||
|
---
|
||||||
|
|
||||||
version: 0.1
|
version: 0.1
|
||||||
log:
|
log:
|
||||||
level: info
|
level: info
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/docker/distribution/digest"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Reference represents a tag or digest within a repository
|
|
||||||
type Reference interface {
|
|
||||||
// HasDigest returns whether the reference has a verifiable
|
|
||||||
// content addressable reference which may be considered secure.
|
|
||||||
HasDigest() bool
|
|
||||||
|
|
||||||
// ImageName returns an image name for the given repository
|
|
||||||
ImageName(string) string
|
|
||||||
|
|
||||||
// Returns a string representation of the reference
|
|
||||||
String() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type tagReference struct {
|
|
||||||
tag string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tr tagReference) HasDigest() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tr tagReference) ImageName(repo string) string {
|
|
||||||
return repo + ":" + tr.tag
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tr tagReference) String() string {
|
|
||||||
return tr.tag
|
|
||||||
}
|
|
||||||
|
|
||||||
type digestReference struct {
|
|
||||||
digest digest.Digest
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dr digestReference) HasDigest() bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dr digestReference) ImageName(repo string) string {
|
|
||||||
return repo + "@" + dr.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dr digestReference) String() string {
|
|
||||||
return dr.digest.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseReference parses a reference into either a digest or tag reference
|
|
||||||
func ParseReference(ref string) Reference {
|
|
||||||
if strings.Contains(ref, ":") {
|
|
||||||
dgst, err := digest.ParseDigest(ref)
|
|
||||||
if err == nil {
|
|
||||||
return digestReference{digest: dgst}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tagReference{tag: ref}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DigestReference creates a digest reference using a digest
|
|
||||||
func DigestReference(dgst digest.Digest) Reference {
|
|
||||||
return digestReference{digest: dgst}
|
|
||||||
}
|
|
190
docs/registry.go
190
docs/registry.go
|
@ -1,190 +0,0 @@
|
||||||
// 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"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/docker/distribution/registry/client/transport"
|
|
||||||
"github.com/docker/go-connections/sockets"
|
|
||||||
"github.com/docker/go-connections/tlsconfig"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrAlreadyExists is an error returned if an image being pushed
|
|
||||||
// already exists on the remote side
|
|
||||||
ErrAlreadyExists = errors.New("Image already exists")
|
|
||||||
)
|
|
||||||
|
|
||||||
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 client certificate %s. Note that CA certificates should use the extension .crt.", 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 client certificate %s for key %s", certName, keyName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DockerHeaders returns request modifiers with a User-Agent and metaHeaders
|
|
||||||
func DockerHeaders(userAgent string, metaHeaders http.Header) []transport.RequestModifier {
|
|
||||||
modifiers := []transport.RequestModifier{}
|
|
||||||
if userAgent != "" {
|
|
||||||
modifiers = append(modifiers, transport.NewHeaderRequestModifier(http.Header{
|
|
||||||
"User-Agent": []string{userAgent},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
if metaHeaders != nil {
|
|
||||||
modifiers = append(modifiers, transport.NewHeaderRequestModifier(metaHeaders))
|
|
||||||
}
|
|
||||||
return modifiers
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTPClient returns an HTTP client structure which uses the given transport
|
|
||||||
// and contains the necessary headers for redirected requests
|
|
||||||
func HTTPClient(transport http.RoundTripper) *http.Client {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
direct := &net.Dialer{
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
KeepAlive: 30 * time.Second,
|
|
||||||
DualStack: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
base := &http.Transport{
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
Dial: direct.Dial,
|
|
||||||
TLSHandshakeTimeout: 10 * time.Second,
|
|
||||||
TLSClientConfig: tlsConfig,
|
|
||||||
// TODO(dmcgowan): Call close idle connections when complete and use keep alive
|
|
||||||
DisableKeepAlives: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyDialer, err := sockets.DialerFromEnvironment(direct)
|
|
||||||
if err == nil {
|
|
||||||
base.Dial = proxyDialer.Dial
|
|
||||||
}
|
|
||||||
return base
|
|
||||||
}
|
|
|
@ -1,476 +0,0 @@
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/docker/docker/reference"
|
|
||||||
registrytypes "github.com/docker/engine-api/types/registry"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
testHTTPServer *httptest.Server
|
|
||||||
testHTTPSServer *httptest.Server
|
|
||||||
testLayers = map[string]map[string]string{
|
|
||||||
"77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20": {
|
|
||||||
"json": `{"id":"77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20",
|
|
||||||
"comment":"test base image","created":"2013-03-23T12:53:11.10432-07:00",
|
|
||||||
"container_config":{"Hostname":"","User":"","Memory":0,"MemorySwap":0,
|
|
||||||
"CpuShares":0,"AttachStdin":false,"AttachStdout":false,"AttachStderr":false,
|
|
||||||
"Tty":false,"OpenStdin":false,"StdinOnce":false,
|
|
||||||
"Env":null,"Cmd":null,"Dns":null,"Image":"","Volumes":null,
|
|
||||||
"VolumesFrom":"","Entrypoint":null},"Size":424242}`,
|
|
||||||
"checksum_simple": "sha256:1ac330d56e05eef6d438586545ceff7550d3bdcb6b19961f12c5ba714ee1bb37",
|
|
||||||
"checksum_tarsum": "tarsum+sha256:4409a0685741ca86d38df878ed6f8cbba4c99de5dc73cd71aef04be3bb70be7c",
|
|
||||||
"ancestry": `["77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20"]`,
|
|
||||||
"layer": string([]byte{
|
|
||||||
0x1f, 0x8b, 0x08, 0x08, 0x0e, 0xb0, 0xee, 0x51, 0x02, 0x03, 0x6c, 0x61, 0x79, 0x65,
|
|
||||||
0x72, 0x2e, 0x74, 0x61, 0x72, 0x00, 0xed, 0xd2, 0x31, 0x0e, 0xc2, 0x30, 0x0c, 0x05,
|
|
||||||
0x50, 0xcf, 0x9c, 0xc2, 0x27, 0x48, 0xed, 0x38, 0x4e, 0xce, 0x13, 0x44, 0x2b, 0x66,
|
|
||||||
0x62, 0x24, 0x8e, 0x4f, 0xa0, 0x15, 0x63, 0xb6, 0x20, 0x21, 0xfc, 0x96, 0xbf, 0x78,
|
|
||||||
0xb0, 0xf5, 0x1d, 0x16, 0x98, 0x8e, 0x88, 0x8a, 0x2a, 0xbe, 0x33, 0xef, 0x49, 0x31,
|
|
||||||
0xed, 0x79, 0x40, 0x8e, 0x5c, 0x44, 0x85, 0x88, 0x33, 0x12, 0x73, 0x2c, 0x02, 0xa8,
|
|
||||||
0xf0, 0x05, 0xf7, 0x66, 0xf5, 0xd6, 0x57, 0x69, 0xd7, 0x7a, 0x19, 0xcd, 0xf5, 0xb1,
|
|
||||||
0x6d, 0x1b, 0x1f, 0xf9, 0xba, 0xe3, 0x93, 0x3f, 0x22, 0x2c, 0xb6, 0x36, 0x0b, 0xf6,
|
|
||||||
0xb0, 0xa9, 0xfd, 0xe7, 0x94, 0x46, 0xfd, 0xeb, 0xd1, 0x7f, 0x2c, 0xc4, 0xd2, 0xfb,
|
|
||||||
0x97, 0xfe, 0x02, 0x80, 0xe4, 0xfd, 0x4f, 0x77, 0xae, 0x6d, 0x3d, 0x81, 0x73, 0xce,
|
|
||||||
0xb9, 0x7f, 0xf3, 0x04, 0x41, 0xc1, 0xab, 0xc6, 0x00, 0x0a, 0x00, 0x00,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
"42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d": {
|
|
||||||
"json": `{"id":"42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d",
|
|
||||||
"parent":"77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20",
|
|
||||||
"comment":"test base image","created":"2013-03-23T12:55:11.10432-07:00",
|
|
||||||
"container_config":{"Hostname":"","User":"","Memory":0,"MemorySwap":0,
|
|
||||||
"CpuShares":0,"AttachStdin":false,"AttachStdout":false,"AttachStderr":false,
|
|
||||||
"Tty":false,"OpenStdin":false,"StdinOnce":false,
|
|
||||||
"Env":null,"Cmd":null,"Dns":null,"Image":"","Volumes":null,
|
|
||||||
"VolumesFrom":"","Entrypoint":null},"Size":424242}`,
|
|
||||||
"checksum_simple": "sha256:bea7bf2e4bacd479344b737328db47b18880d09096e6674165533aa994f5e9f2",
|
|
||||||
"checksum_tarsum": "tarsum+sha256:68fdb56fb364f074eec2c9b3f85ca175329c4dcabc4a6a452b7272aa613a07a2",
|
|
||||||
"ancestry": `["42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d",
|
|
||||||
"77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20"]`,
|
|
||||||
"layer": string([]byte{
|
|
||||||
0x1f, 0x8b, 0x08, 0x08, 0xbd, 0xb3, 0xee, 0x51, 0x02, 0x03, 0x6c, 0x61, 0x79, 0x65,
|
|
||||||
0x72, 0x2e, 0x74, 0x61, 0x72, 0x00, 0xed, 0xd1, 0x31, 0x0e, 0xc2, 0x30, 0x0c, 0x05,
|
|
||||||
0x50, 0xcf, 0x9c, 0xc2, 0x27, 0x48, 0x9d, 0x38, 0x8e, 0xcf, 0x53, 0x51, 0xaa, 0x56,
|
|
||||||
0xea, 0x44, 0x82, 0xc4, 0xf1, 0x09, 0xb4, 0xea, 0x98, 0x2d, 0x48, 0x08, 0xbf, 0xe5,
|
|
||||||
0x2f, 0x1e, 0xfc, 0xf5, 0xdd, 0x00, 0xdd, 0x11, 0x91, 0x8a, 0xe0, 0x27, 0xd3, 0x9e,
|
|
||||||
0x14, 0xe2, 0x9e, 0x07, 0xf4, 0xc1, 0x2b, 0x0b, 0xfb, 0xa4, 0x82, 0xe4, 0x3d, 0x93,
|
|
||||||
0x02, 0x0a, 0x7c, 0xc1, 0x23, 0x97, 0xf1, 0x5e, 0x5f, 0xc9, 0xcb, 0x38, 0xb5, 0xee,
|
|
||||||
0xea, 0xd9, 0x3c, 0xb7, 0x4b, 0xbe, 0x7b, 0x9c, 0xf9, 0x23, 0xdc, 0x50, 0x6e, 0xb9,
|
|
||||||
0xb8, 0xf2, 0x2c, 0x5d, 0xf7, 0x4f, 0x31, 0xb6, 0xf6, 0x4f, 0xc7, 0xfe, 0x41, 0x55,
|
|
||||||
0x63, 0xdd, 0x9f, 0x89, 0x09, 0x90, 0x6c, 0xff, 0xee, 0xae, 0xcb, 0xba, 0x4d, 0x17,
|
|
||||||
0x30, 0xc6, 0x18, 0xf3, 0x67, 0x5e, 0xc1, 0xed, 0x21, 0x5d, 0x00, 0x0a, 0x00, 0x00,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
testRepositories = map[string]map[string]string{
|
|
||||||
"foo42/bar": {
|
|
||||||
"latest": "42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d",
|
|
||||||
"test": "42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
mockHosts = map[string][]net.IP{
|
|
||||||
"": {net.ParseIP("0.0.0.0")},
|
|
||||||
"localhost": {net.ParseIP("127.0.0.1"), net.ParseIP("::1")},
|
|
||||||
"example.com": {net.ParseIP("42.42.42.42")},
|
|
||||||
"other.com": {net.ParseIP("43.43.43.43")},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
r := mux.NewRouter()
|
|
||||||
|
|
||||||
// /v1/
|
|
||||||
r.HandleFunc("/v1/_ping", handlerGetPing).Methods("GET")
|
|
||||||
r.HandleFunc("/v1/images/{image_id:[^/]+}/{action:json|layer|ancestry}", handlerGetImage).Methods("GET")
|
|
||||||
r.HandleFunc("/v1/images/{image_id:[^/]+}/{action:json|layer|checksum}", handlerPutImage).Methods("PUT")
|
|
||||||
r.HandleFunc("/v1/repositories/{repository:.+}/tags", handlerGetDeleteTags).Methods("GET", "DELETE")
|
|
||||||
r.HandleFunc("/v1/repositories/{repository:.+}/tags/{tag:.+}", handlerGetTag).Methods("GET")
|
|
||||||
r.HandleFunc("/v1/repositories/{repository:.+}/tags/{tag:.+}", handlerPutTag).Methods("PUT")
|
|
||||||
r.HandleFunc("/v1/users{null:.*}", handlerUsers).Methods("GET", "POST", "PUT")
|
|
||||||
r.HandleFunc("/v1/repositories/{repository:.+}{action:/images|/}", handlerImages).Methods("GET", "PUT", "DELETE")
|
|
||||||
r.HandleFunc("/v1/repositories/{repository:.+}/auth", handlerAuth).Methods("PUT")
|
|
||||||
r.HandleFunc("/v1/search", handlerSearch).Methods("GET")
|
|
||||||
|
|
||||||
// /v2/
|
|
||||||
r.HandleFunc("/v2/version", handlerGetPing).Methods("GET")
|
|
||||||
|
|
||||||
testHTTPServer = httptest.NewServer(handlerAccessLog(r))
|
|
||||||
testHTTPSServer = httptest.NewTLSServer(handlerAccessLog(r))
|
|
||||||
|
|
||||||
// override net.LookupIP
|
|
||||||
lookupIP = func(host string) ([]net.IP, error) {
|
|
||||||
if host == "127.0.0.1" {
|
|
||||||
// I believe in future Go versions this will fail, so let's fix it later
|
|
||||||
return net.LookupIP(host)
|
|
||||||
}
|
|
||||||
for h, addrs := range mockHosts {
|
|
||||||
if host == h {
|
|
||||||
return addrs, nil
|
|
||||||
}
|
|
||||||
for _, addr := range addrs {
|
|
||||||
if addr.String() == host {
|
|
||||||
return []net.IP{addr}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, errors.New("lookup: no such host")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlerAccessLog(handler http.Handler) http.Handler {
|
|
||||||
logHandler := func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
logrus.Debugf("%s \"%s %s\"", r.RemoteAddr, r.Method, r.URL)
|
|
||||||
handler.ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
return http.HandlerFunc(logHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeURL(req string) string {
|
|
||||||
return testHTTPServer.URL + req
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeHTTPSURL(req string) string {
|
|
||||||
return testHTTPSServer.URL + req
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeIndex(req string) *registrytypes.IndexInfo {
|
|
||||||
index := ®istrytypes.IndexInfo{
|
|
||||||
Name: makeURL(req),
|
|
||||||
}
|
|
||||||
return index
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeHTTPSIndex(req string) *registrytypes.IndexInfo {
|
|
||||||
index := ®istrytypes.IndexInfo{
|
|
||||||
Name: makeHTTPSURL(req),
|
|
||||||
}
|
|
||||||
return index
|
|
||||||
}
|
|
||||||
|
|
||||||
func makePublicIndex() *registrytypes.IndexInfo {
|
|
||||||
index := ®istrytypes.IndexInfo{
|
|
||||||
Name: IndexServer,
|
|
||||||
Secure: true,
|
|
||||||
Official: true,
|
|
||||||
}
|
|
||||||
return index
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeServiceConfig(mirrors []string, insecureRegistries []string) *serviceConfig {
|
|
||||||
options := ServiceOptions{
|
|
||||||
Mirrors: mirrors,
|
|
||||||
InsecureRegistries: insecureRegistries,
|
|
||||||
}
|
|
||||||
|
|
||||||
return newServiceConfig(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeHeaders(w http.ResponseWriter) {
|
|
||||||
h := w.Header()
|
|
||||||
h.Add("Server", "docker-tests/mock")
|
|
||||||
h.Add("Expires", "-1")
|
|
||||||
h.Add("Content-Type", "application/json")
|
|
||||||
h.Add("Pragma", "no-cache")
|
|
||||||
h.Add("Cache-Control", "no-cache")
|
|
||||||
h.Add("X-Docker-Registry-Version", "0.0.0")
|
|
||||||
h.Add("X-Docker-Registry-Config", "mock")
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeResponse(w http.ResponseWriter, message interface{}, code int) {
|
|
||||||
writeHeaders(w)
|
|
||||||
w.WriteHeader(code)
|
|
||||||
body, err := json.Marshal(message)
|
|
||||||
if err != nil {
|
|
||||||
io.WriteString(w, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Write(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
func readJSON(r *http.Request, dest interface{}) error {
|
|
||||||
body, err := ioutil.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return json.Unmarshal(body, dest)
|
|
||||||
}
|
|
||||||
|
|
||||||
func apiError(w http.ResponseWriter, message string, code int) {
|
|
||||||
body := map[string]string{
|
|
||||||
"error": message,
|
|
||||||
}
|
|
||||||
writeResponse(w, body, code)
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertEqual(t *testing.T, a interface{}, b interface{}, message string) {
|
|
||||||
if a == b {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(message) == 0 {
|
|
||||||
message = fmt.Sprintf("%v != %v", a, b)
|
|
||||||
}
|
|
||||||
t.Fatal(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertNotEqual(t *testing.T, a interface{}, b interface{}, message string) {
|
|
||||||
if a != b {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(message) == 0 {
|
|
||||||
message = fmt.Sprintf("%v == %v", a, b)
|
|
||||||
}
|
|
||||||
t.Fatal(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Similar to assertEqual, but does not stop test
|
|
||||||
func checkEqual(t *testing.T, a interface{}, b interface{}, messagePrefix string) {
|
|
||||||
if a == b {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
message := fmt.Sprintf("%v != %v", a, b)
|
|
||||||
if len(messagePrefix) != 0 {
|
|
||||||
message = messagePrefix + ": " + message
|
|
||||||
}
|
|
||||||
t.Error(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Similar to assertNotEqual, but does not stop test
|
|
||||||
func checkNotEqual(t *testing.T, a interface{}, b interface{}, messagePrefix string) {
|
|
||||||
if a != b {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
message := fmt.Sprintf("%v == %v", a, b)
|
|
||||||
if len(messagePrefix) != 0 {
|
|
||||||
message = messagePrefix + ": " + message
|
|
||||||
}
|
|
||||||
t.Error(message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func requiresAuth(w http.ResponseWriter, r *http.Request) bool {
|
|
||||||
writeCookie := func() {
|
|
||||||
value := fmt.Sprintf("FAKE-SESSION-%d", time.Now().UnixNano())
|
|
||||||
cookie := &http.Cookie{Name: "session", Value: value, MaxAge: 3600}
|
|
||||||
http.SetCookie(w, cookie)
|
|
||||||
//FIXME(sam): this should be sent only on Index routes
|
|
||||||
value = fmt.Sprintf("FAKE-TOKEN-%d", time.Now().UnixNano())
|
|
||||||
w.Header().Add("X-Docker-Token", value)
|
|
||||||
}
|
|
||||||
if len(r.Cookies()) > 0 {
|
|
||||||
writeCookie()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if len(r.Header.Get("Authorization")) > 0 {
|
|
||||||
writeCookie()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
w.Header().Add("WWW-Authenticate", "token")
|
|
||||||
apiError(w, "Wrong auth", 401)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlerGetPing(w http.ResponseWriter, r *http.Request) {
|
|
||||||
writeResponse(w, true, 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlerGetImage(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !requiresAuth(w, r) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
layer, exists := testLayers[vars["image_id"]]
|
|
||||||
if !exists {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeHeaders(w)
|
|
||||||
layerSize := len(layer["layer"])
|
|
||||||
w.Header().Add("X-Docker-Size", strconv.Itoa(layerSize))
|
|
||||||
io.WriteString(w, layer[vars["action"]])
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlerPutImage(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !requiresAuth(w, r) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
imageID := vars["image_id"]
|
|
||||||
action := vars["action"]
|
|
||||||
layer, exists := testLayers[imageID]
|
|
||||||
if !exists {
|
|
||||||
if action != "json" {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
layer = make(map[string]string)
|
|
||||||
testLayers[imageID] = layer
|
|
||||||
}
|
|
||||||
if checksum := r.Header.Get("X-Docker-Checksum"); checksum != "" {
|
|
||||||
if checksum != layer["checksum_simple"] && checksum != layer["checksum_tarsum"] {
|
|
||||||
apiError(w, "Wrong checksum", 400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
body, err := ioutil.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
apiError(w, fmt.Sprintf("Error: %s", err), 500)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
layer[action] = string(body)
|
|
||||||
writeResponse(w, true, 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlerGetDeleteTags(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !requiresAuth(w, r) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
repositoryName, err := reference.WithName(mux.Vars(r)["repository"])
|
|
||||||
if err != nil {
|
|
||||||
apiError(w, "Could not parse repository", 400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tags, exists := testRepositories[repositoryName.String()]
|
|
||||||
if !exists {
|
|
||||||
apiError(w, "Repository not found", 404)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.Method == "DELETE" {
|
|
||||||
delete(testRepositories, repositoryName.String())
|
|
||||||
writeResponse(w, true, 200)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeResponse(w, tags, 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlerGetTag(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !requiresAuth(w, r) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
repositoryName, err := reference.WithName(vars["repository"])
|
|
||||||
if err != nil {
|
|
||||||
apiError(w, "Could not parse repository", 400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tagName := vars["tag"]
|
|
||||||
tags, exists := testRepositories[repositoryName.String()]
|
|
||||||
if !exists {
|
|
||||||
apiError(w, "Repository not found", 404)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tag, exists := tags[tagName]
|
|
||||||
if !exists {
|
|
||||||
apiError(w, "Tag not found", 404)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeResponse(w, tag, 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlerPutTag(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if !requiresAuth(w, r) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
repositoryName, err := reference.WithName(vars["repository"])
|
|
||||||
if err != nil {
|
|
||||||
apiError(w, "Could not parse repository", 400)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
tagName := vars["tag"]
|
|
||||||
tags, exists := testRepositories[repositoryName.String()]
|
|
||||||
if !exists {
|
|
||||||
tags = make(map[string]string)
|
|
||||||
testRepositories[repositoryName.String()] = tags
|
|
||||||
}
|
|
||||||
tagValue := ""
|
|
||||||
readJSON(r, tagValue)
|
|
||||||
tags[tagName] = tagValue
|
|
||||||
writeResponse(w, true, 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlerUsers(w http.ResponseWriter, r *http.Request) {
|
|
||||||
code := 200
|
|
||||||
if r.Method == "POST" {
|
|
||||||
code = 201
|
|
||||||
} else if r.Method == "PUT" {
|
|
||||||
code = 204
|
|
||||||
}
|
|
||||||
writeResponse(w, "", code)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlerImages(w http.ResponseWriter, r *http.Request) {
|
|
||||||
u, _ := url.Parse(testHTTPServer.URL)
|
|
||||||
w.Header().Add("X-Docker-Endpoints", fmt.Sprintf("%s , %s ", u.Host, "test.example.com"))
|
|
||||||
w.Header().Add("X-Docker-Token", fmt.Sprintf("FAKE-SESSION-%d", time.Now().UnixNano()))
|
|
||||||
if r.Method == "PUT" {
|
|
||||||
if strings.HasSuffix(r.URL.Path, "images") {
|
|
||||||
writeResponse(w, "", 204)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writeResponse(w, "", 200)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if r.Method == "DELETE" {
|
|
||||||
writeResponse(w, "", 204)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
images := []map[string]string{}
|
|
||||||
for imageID, layer := range testLayers {
|
|
||||||
image := make(map[string]string)
|
|
||||||
image["id"] = imageID
|
|
||||||
image["checksum"] = layer["checksum_tarsum"]
|
|
||||||
image["Tag"] = "latest"
|
|
||||||
images = append(images, image)
|
|
||||||
}
|
|
||||||
writeResponse(w, images, 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlerAuth(w http.ResponseWriter, r *http.Request) {
|
|
||||||
writeResponse(w, "OK", 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
func handlerSearch(w http.ResponseWriter, r *http.Request) {
|
|
||||||
result := ®istrytypes.SearchResults{
|
|
||||||
Query: "fakequery",
|
|
||||||
NumResults: 1,
|
|
||||||
Results: []registrytypes.SearchResult{{Name: "fakeimage", StarCount: 42}},
|
|
||||||
}
|
|
||||||
writeResponse(w, result, 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPing(t *testing.T) {
|
|
||||||
res, err := http.Get(makeURL("/v1/_ping"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
assertEqual(t, res.StatusCode, 200, "")
|
|
||||||
assertEqual(t, res.Header.Get("X-Docker-Registry-Config"), "mock",
|
|
||||||
"This is not a Mocked Registry")
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Uncomment this to test Mocked Registry locally with curl
|
|
||||||
* WARNING: Don't push on the repos uncommented, it'll block the tests
|
|
||||||
*
|
|
||||||
func TestWait(t *testing.T) {
|
|
||||||
logrus.Println("Test HTTP server ready and waiting:", testHTTPServer.URL)
|
|
||||||
c := make(chan int)
|
|
||||||
<-c
|
|
||||||
}
|
|
||||||
|
|
||||||
//*/
|
|
|
@ -1,873 +0,0 @@
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httputil"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/docker/distribution/registry/client/transport"
|
|
||||||
"github.com/docker/docker/reference"
|
|
||||||
"github.com/docker/engine-api/types"
|
|
||||||
registrytypes "github.com/docker/engine-api/types/registry"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
token = []string{"fake-token"}
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
imageID = "42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d"
|
|
||||||
REPO = "foo42/bar"
|
|
||||||
)
|
|
||||||
|
|
||||||
func spawnTestRegistrySession(t *testing.T) *Session {
|
|
||||||
authConfig := &types.AuthConfig{}
|
|
||||||
endpoint, err := NewV1Endpoint(makeIndex("/v1/"), "", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
userAgent := "docker test client"
|
|
||||||
var tr http.RoundTripper = debugTransport{NewTransport(nil), t.Log}
|
|
||||||
tr = transport.NewTransport(AuthTransport(tr, authConfig, false), DockerHeaders(userAgent, nil)...)
|
|
||||||
client := HTTPClient(tr)
|
|
||||||
r, err := NewSession(client, authConfig, endpoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
// In a normal scenario for the v1 registry, the client should send a `X-Docker-Token: true`
|
|
||||||
// header while authenticating, in order to retrieve a token that can be later used to
|
|
||||||
// perform authenticated actions.
|
|
||||||
//
|
|
||||||
// The mock v1 registry does not support that, (TODO(tiborvass): support it), instead,
|
|
||||||
// it will consider authenticated any request with the header `X-Docker-Token: fake-token`.
|
|
||||||
//
|
|
||||||
// Because we know that the client's transport is an `*authTransport` we simply cast it,
|
|
||||||
// in order to set the internal cached token to the fake token, and thus send that fake token
|
|
||||||
// upon every subsequent requests.
|
|
||||||
r.client.Transport.(*authTransport).token = token
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPingRegistryEndpoint(t *testing.T) {
|
|
||||||
testPing := func(index *registrytypes.IndexInfo, expectedStandalone bool, assertMessage string) {
|
|
||||||
ep, err := NewV1Endpoint(index, "", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
regInfo, err := ep.Ping()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertEqual(t, regInfo.Standalone, expectedStandalone, assertMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
testPing(makeIndex("/v1/"), true, "Expected standalone to be true (default)")
|
|
||||||
testPing(makeHTTPSIndex("/v1/"), true, "Expected standalone to be true (default)")
|
|
||||||
testPing(makePublicIndex(), false, "Expected standalone to be false for public index")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEndpoint(t *testing.T) {
|
|
||||||
// Simple wrapper to fail test if err != nil
|
|
||||||
expandEndpoint := func(index *registrytypes.IndexInfo) *V1Endpoint {
|
|
||||||
endpoint, err := NewV1Endpoint(index, "", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
return endpoint
|
|
||||||
}
|
|
||||||
|
|
||||||
assertInsecureIndex := func(index *registrytypes.IndexInfo) {
|
|
||||||
index.Secure = true
|
|
||||||
_, err := NewV1Endpoint(index, "", nil)
|
|
||||||
assertNotEqual(t, err, nil, index.Name+": Expected error for insecure index")
|
|
||||||
assertEqual(t, strings.Contains(err.Error(), "insecure-registry"), true, index.Name+": Expected insecure-registry error for insecure index")
|
|
||||||
index.Secure = false
|
|
||||||
}
|
|
||||||
|
|
||||||
assertSecureIndex := func(index *registrytypes.IndexInfo) {
|
|
||||||
index.Secure = true
|
|
||||||
_, err := NewV1Endpoint(index, "", nil)
|
|
||||||
assertNotEqual(t, err, nil, index.Name+": Expected cert error for secure index")
|
|
||||||
assertEqual(t, strings.Contains(err.Error(), "certificate signed by unknown authority"), true, index.Name+": Expected cert error for secure index")
|
|
||||||
index.Secure = false
|
|
||||||
}
|
|
||||||
|
|
||||||
index := ®istrytypes.IndexInfo{}
|
|
||||||
index.Name = makeURL("/v1/")
|
|
||||||
endpoint := expandEndpoint(index)
|
|
||||||
assertEqual(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name)
|
|
||||||
assertInsecureIndex(index)
|
|
||||||
|
|
||||||
index.Name = makeURL("")
|
|
||||||
endpoint = expandEndpoint(index)
|
|
||||||
assertEqual(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/")
|
|
||||||
assertInsecureIndex(index)
|
|
||||||
|
|
||||||
httpURL := makeURL("")
|
|
||||||
index.Name = strings.SplitN(httpURL, "://", 2)[1]
|
|
||||||
endpoint = expandEndpoint(index)
|
|
||||||
assertEqual(t, endpoint.String(), httpURL+"/v1/", index.Name+": Expected endpoint to be "+httpURL+"/v1/")
|
|
||||||
assertInsecureIndex(index)
|
|
||||||
|
|
||||||
index.Name = makeHTTPSURL("/v1/")
|
|
||||||
endpoint = expandEndpoint(index)
|
|
||||||
assertEqual(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name)
|
|
||||||
assertSecureIndex(index)
|
|
||||||
|
|
||||||
index.Name = makeHTTPSURL("")
|
|
||||||
endpoint = expandEndpoint(index)
|
|
||||||
assertEqual(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/")
|
|
||||||
assertSecureIndex(index)
|
|
||||||
|
|
||||||
httpsURL := makeHTTPSURL("")
|
|
||||||
index.Name = strings.SplitN(httpsURL, "://", 2)[1]
|
|
||||||
endpoint = expandEndpoint(index)
|
|
||||||
assertEqual(t, endpoint.String(), httpsURL+"/v1/", index.Name+": Expected endpoint to be "+httpsURL+"/v1/")
|
|
||||||
assertSecureIndex(index)
|
|
||||||
|
|
||||||
badEndpoints := []string{
|
|
||||||
"http://127.0.0.1/v1/",
|
|
||||||
"https://127.0.0.1/v1/",
|
|
||||||
"http://127.0.0.1",
|
|
||||||
"https://127.0.0.1",
|
|
||||||
"127.0.0.1",
|
|
||||||
}
|
|
||||||
for _, address := range badEndpoints {
|
|
||||||
index.Name = address
|
|
||||||
_, err := NewV1Endpoint(index, "", nil)
|
|
||||||
checkNotEqual(t, err, nil, "Expected error while expanding bad endpoint")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetRemoteHistory(t *testing.T) {
|
|
||||||
r := spawnTestRegistrySession(t)
|
|
||||||
hist, err := r.GetRemoteHistory(imageID, makeURL("/v1/"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
assertEqual(t, len(hist), 2, "Expected 2 images in history")
|
|
||||||
assertEqual(t, hist[0], imageID, "Expected "+imageID+"as first ancestry")
|
|
||||||
assertEqual(t, hist[1], "77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20",
|
|
||||||
"Unexpected second ancestry")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestLookupRemoteImage(t *testing.T) {
|
|
||||||
r := spawnTestRegistrySession(t)
|
|
||||||
err := r.LookupRemoteImage(imageID, makeURL("/v1/"))
|
|
||||||
assertEqual(t, err, nil, "Expected error of remote lookup to nil")
|
|
||||||
if err := r.LookupRemoteImage("abcdef", makeURL("/v1/")); err == nil {
|
|
||||||
t.Fatal("Expected error of remote lookup to not nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetRemoteImageJSON(t *testing.T) {
|
|
||||||
r := spawnTestRegistrySession(t)
|
|
||||||
json, size, err := r.GetRemoteImageJSON(imageID, makeURL("/v1/"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
assertEqual(t, size, int64(154), "Expected size 154")
|
|
||||||
if len(json) == 0 {
|
|
||||||
t.Fatal("Expected non-empty json")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _, err = r.GetRemoteImageJSON("abcdef", makeURL("/v1/"))
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("Expected image not found error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetRemoteImageLayer(t *testing.T) {
|
|
||||||
r := spawnTestRegistrySession(t)
|
|
||||||
data, err := r.GetRemoteImageLayer(imageID, makeURL("/v1/"), 0)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if data == nil {
|
|
||||||
t.Fatal("Expected non-nil data result")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = r.GetRemoteImageLayer("abcdef", makeURL("/v1/"), 0)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("Expected image not found error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetRemoteTag(t *testing.T) {
|
|
||||||
r := spawnTestRegistrySession(t)
|
|
||||||
repoRef, err := reference.ParseNamed(REPO)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
tag, err := r.GetRemoteTag([]string{makeURL("/v1/")}, repoRef, "test")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
assertEqual(t, tag, imageID, "Expected tag test to map to "+imageID)
|
|
||||||
|
|
||||||
bazRef, err := reference.ParseNamed("foo42/baz")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
_, err = r.GetRemoteTag([]string{makeURL("/v1/")}, bazRef, "foo")
|
|
||||||
if err != ErrRepoNotFound {
|
|
||||||
t.Fatal("Expected ErrRepoNotFound error when fetching tag for bogus repo")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetRemoteTags(t *testing.T) {
|
|
||||||
r := spawnTestRegistrySession(t)
|
|
||||||
repoRef, err := reference.ParseNamed(REPO)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
tags, err := r.GetRemoteTags([]string{makeURL("/v1/")}, repoRef)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
assertEqual(t, len(tags), 2, "Expected two tags")
|
|
||||||
assertEqual(t, tags["latest"], imageID, "Expected tag latest to map to "+imageID)
|
|
||||||
assertEqual(t, tags["test"], imageID, "Expected tag test to map to "+imageID)
|
|
||||||
|
|
||||||
bazRef, err := reference.ParseNamed("foo42/baz")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
_, err = r.GetRemoteTags([]string{makeURL("/v1/")}, bazRef)
|
|
||||||
if err != ErrRepoNotFound {
|
|
||||||
t.Fatal("Expected ErrRepoNotFound error when fetching tags for bogus repo")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetRepositoryData(t *testing.T) {
|
|
||||||
r := spawnTestRegistrySession(t)
|
|
||||||
parsedURL, err := url.Parse(makeURL("/v1/"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
host := "http://" + parsedURL.Host + "/v1/"
|
|
||||||
repoRef, err := reference.ParseNamed(REPO)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
data, err := r.GetRepositoryData(repoRef)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
assertEqual(t, len(data.ImgList), 2, "Expected 2 images in ImgList")
|
|
||||||
assertEqual(t, len(data.Endpoints), 2,
|
|
||||||
fmt.Sprintf("Expected 2 endpoints in Endpoints, found %d instead", len(data.Endpoints)))
|
|
||||||
assertEqual(t, data.Endpoints[0], host,
|
|
||||||
fmt.Sprintf("Expected first endpoint to be %s but found %s instead", host, data.Endpoints[0]))
|
|
||||||
assertEqual(t, data.Endpoints[1], "http://test.example.com/v1/",
|
|
||||||
fmt.Sprintf("Expected first endpoint to be http://test.example.com/v1/ but found %s instead", data.Endpoints[1]))
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPushImageJSONRegistry(t *testing.T) {
|
|
||||||
r := spawnTestRegistrySession(t)
|
|
||||||
imgData := &ImgData{
|
|
||||||
ID: "77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20",
|
|
||||||
Checksum: "sha256:1ac330d56e05eef6d438586545ceff7550d3bdcb6b19961f12c5ba714ee1bb37",
|
|
||||||
}
|
|
||||||
|
|
||||||
err := r.PushImageJSONRegistry(imgData, []byte{0x42, 0xdf, 0x0}, makeURL("/v1/"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPushImageLayerRegistry(t *testing.T) {
|
|
||||||
r := spawnTestRegistrySession(t)
|
|
||||||
layer := strings.NewReader("")
|
|
||||||
_, _, err := r.PushImageLayerRegistry(imageID, layer, makeURL("/v1/"), []byte{})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseRepositoryInfo(t *testing.T) {
|
|
||||||
type staticRepositoryInfo struct {
|
|
||||||
Index *registrytypes.IndexInfo
|
|
||||||
RemoteName string
|
|
||||||
CanonicalName string
|
|
||||||
LocalName string
|
|
||||||
Official bool
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedRepoInfos := map[string]staticRepositoryInfo{
|
|
||||||
"fooo/bar": {
|
|
||||||
Index: ®istrytypes.IndexInfo{
|
|
||||||
Name: IndexName,
|
|
||||||
Official: true,
|
|
||||||
},
|
|
||||||
RemoteName: "fooo/bar",
|
|
||||||
LocalName: "fooo/bar",
|
|
||||||
CanonicalName: "docker.io/fooo/bar",
|
|
||||||
Official: false,
|
|
||||||
},
|
|
||||||
"library/ubuntu": {
|
|
||||||
Index: ®istrytypes.IndexInfo{
|
|
||||||
Name: IndexName,
|
|
||||||
Official: true,
|
|
||||||
},
|
|
||||||
RemoteName: "library/ubuntu",
|
|
||||||
LocalName: "ubuntu",
|
|
||||||
CanonicalName: "docker.io/library/ubuntu",
|
|
||||||
Official: true,
|
|
||||||
},
|
|
||||||
"nonlibrary/ubuntu": {
|
|
||||||
Index: ®istrytypes.IndexInfo{
|
|
||||||
Name: IndexName,
|
|
||||||
Official: true,
|
|
||||||
},
|
|
||||||
RemoteName: "nonlibrary/ubuntu",
|
|
||||||
LocalName: "nonlibrary/ubuntu",
|
|
||||||
CanonicalName: "docker.io/nonlibrary/ubuntu",
|
|
||||||
Official: false,
|
|
||||||
},
|
|
||||||
"ubuntu": {
|
|
||||||
Index: ®istrytypes.IndexInfo{
|
|
||||||
Name: IndexName,
|
|
||||||
Official: true,
|
|
||||||
},
|
|
||||||
RemoteName: "library/ubuntu",
|
|
||||||
LocalName: "ubuntu",
|
|
||||||
CanonicalName: "docker.io/library/ubuntu",
|
|
||||||
Official: true,
|
|
||||||
},
|
|
||||||
"other/library": {
|
|
||||||
Index: ®istrytypes.IndexInfo{
|
|
||||||
Name: IndexName,
|
|
||||||
Official: true,
|
|
||||||
},
|
|
||||||
RemoteName: "other/library",
|
|
||||||
LocalName: "other/library",
|
|
||||||
CanonicalName: "docker.io/other/library",
|
|
||||||
Official: false,
|
|
||||||
},
|
|
||||||
"127.0.0.1:8000/private/moonbase": {
|
|
||||||
Index: ®istrytypes.IndexInfo{
|
|
||||||
Name: "127.0.0.1:8000",
|
|
||||||
Official: false,
|
|
||||||
},
|
|
||||||
RemoteName: "private/moonbase",
|
|
||||||
LocalName: "127.0.0.1:8000/private/moonbase",
|
|
||||||
CanonicalName: "127.0.0.1:8000/private/moonbase",
|
|
||||||
Official: false,
|
|
||||||
},
|
|
||||||
"127.0.0.1:8000/privatebase": {
|
|
||||||
Index: ®istrytypes.IndexInfo{
|
|
||||||
Name: "127.0.0.1:8000",
|
|
||||||
Official: false,
|
|
||||||
},
|
|
||||||
RemoteName: "privatebase",
|
|
||||||
LocalName: "127.0.0.1:8000/privatebase",
|
|
||||||
CanonicalName: "127.0.0.1:8000/privatebase",
|
|
||||||
Official: false,
|
|
||||||
},
|
|
||||||
"localhost:8000/private/moonbase": {
|
|
||||||
Index: ®istrytypes.IndexInfo{
|
|
||||||
Name: "localhost:8000",
|
|
||||||
Official: false,
|
|
||||||
},
|
|
||||||
RemoteName: "private/moonbase",
|
|
||||||
LocalName: "localhost:8000/private/moonbase",
|
|
||||||
CanonicalName: "localhost:8000/private/moonbase",
|
|
||||||
Official: false,
|
|
||||||
},
|
|
||||||
"localhost:8000/privatebase": {
|
|
||||||
Index: ®istrytypes.IndexInfo{
|
|
||||||
Name: "localhost:8000",
|
|
||||||
Official: false,
|
|
||||||
},
|
|
||||||
RemoteName: "privatebase",
|
|
||||||
LocalName: "localhost:8000/privatebase",
|
|
||||||
CanonicalName: "localhost:8000/privatebase",
|
|
||||||
Official: false,
|
|
||||||
},
|
|
||||||
"example.com/private/moonbase": {
|
|
||||||
Index: ®istrytypes.IndexInfo{
|
|
||||||
Name: "example.com",
|
|
||||||
Official: false,
|
|
||||||
},
|
|
||||||
RemoteName: "private/moonbase",
|
|
||||||
LocalName: "example.com/private/moonbase",
|
|
||||||
CanonicalName: "example.com/private/moonbase",
|
|
||||||
Official: false,
|
|
||||||
},
|
|
||||||
"example.com/privatebase": {
|
|
||||||
Index: ®istrytypes.IndexInfo{
|
|
||||||
Name: "example.com",
|
|
||||||
Official: false,
|
|
||||||
},
|
|
||||||
RemoteName: "privatebase",
|
|
||||||
LocalName: "example.com/privatebase",
|
|
||||||
CanonicalName: "example.com/privatebase",
|
|
||||||
Official: false,
|
|
||||||
},
|
|
||||||
"example.com:8000/private/moonbase": {
|
|
||||||
Index: ®istrytypes.IndexInfo{
|
|
||||||
Name: "example.com:8000",
|
|
||||||
Official: false,
|
|
||||||
},
|
|
||||||
RemoteName: "private/moonbase",
|
|
||||||
LocalName: "example.com:8000/private/moonbase",
|
|
||||||
CanonicalName: "example.com:8000/private/moonbase",
|
|
||||||
Official: false,
|
|
||||||
},
|
|
||||||
"example.com:8000/privatebase": {
|
|
||||||
Index: ®istrytypes.IndexInfo{
|
|
||||||
Name: "example.com:8000",
|
|
||||||
Official: false,
|
|
||||||
},
|
|
||||||
RemoteName: "privatebase",
|
|
||||||
LocalName: "example.com:8000/privatebase",
|
|
||||||
CanonicalName: "example.com:8000/privatebase",
|
|
||||||
Official: false,
|
|
||||||
},
|
|
||||||
"localhost/private/moonbase": {
|
|
||||||
Index: ®istrytypes.IndexInfo{
|
|
||||||
Name: "localhost",
|
|
||||||
Official: false,
|
|
||||||
},
|
|
||||||
RemoteName: "private/moonbase",
|
|
||||||
LocalName: "localhost/private/moonbase",
|
|
||||||
CanonicalName: "localhost/private/moonbase",
|
|
||||||
Official: false,
|
|
||||||
},
|
|
||||||
"localhost/privatebase": {
|
|
||||||
Index: ®istrytypes.IndexInfo{
|
|
||||||
Name: "localhost",
|
|
||||||
Official: false,
|
|
||||||
},
|
|
||||||
RemoteName: "privatebase",
|
|
||||||
LocalName: "localhost/privatebase",
|
|
||||||
CanonicalName: "localhost/privatebase",
|
|
||||||
Official: false,
|
|
||||||
},
|
|
||||||
IndexName + "/public/moonbase": {
|
|
||||||
Index: ®istrytypes.IndexInfo{
|
|
||||||
Name: IndexName,
|
|
||||||
Official: true,
|
|
||||||
},
|
|
||||||
RemoteName: "public/moonbase",
|
|
||||||
LocalName: "public/moonbase",
|
|
||||||
CanonicalName: "docker.io/public/moonbase",
|
|
||||||
Official: false,
|
|
||||||
},
|
|
||||||
"index." + IndexName + "/public/moonbase": {
|
|
||||||
Index: ®istrytypes.IndexInfo{
|
|
||||||
Name: IndexName,
|
|
||||||
Official: true,
|
|
||||||
},
|
|
||||||
RemoteName: "public/moonbase",
|
|
||||||
LocalName: "public/moonbase",
|
|
||||||
CanonicalName: "docker.io/public/moonbase",
|
|
||||||
Official: false,
|
|
||||||
},
|
|
||||||
"ubuntu-12.04-base": {
|
|
||||||
Index: ®istrytypes.IndexInfo{
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
IndexName + "/ubuntu-12.04-base": {
|
|
||||||
Index: ®istrytypes.IndexInfo{
|
|
||||||
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." + IndexName + "/ubuntu-12.04-base": {
|
|
||||||
Index: ®istrytypes.IndexInfo{
|
|
||||||
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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for reposName, expectedRepoInfo := range expectedRepoInfos {
|
|
||||||
named, err := reference.WithName(reposName)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
repoInfo, err := ParseRepositoryInfo(named)
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
} else {
|
|
||||||
checkEqual(t, repoInfo.Index.Name, expectedRepoInfo.Index.Name, reposName)
|
|
||||||
checkEqual(t, repoInfo.RemoteName(), expectedRepoInfo.RemoteName, reposName)
|
|
||||||
checkEqual(t, repoInfo.Name(), expectedRepoInfo.LocalName, reposName)
|
|
||||||
checkEqual(t, repoInfo.FullName(), expectedRepoInfo.CanonicalName, reposName)
|
|
||||||
checkEqual(t, repoInfo.Index.Official, expectedRepoInfo.Index.Official, reposName)
|
|
||||||
checkEqual(t, repoInfo.Official, expectedRepoInfo.Official, reposName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewIndexInfo(t *testing.T) {
|
|
||||||
testIndexInfo := func(config *serviceConfig, expectedIndexInfos map[string]*registrytypes.IndexInfo) {
|
|
||||||
for indexName, expectedIndexInfo := range expectedIndexInfos {
|
|
||||||
index, err := newIndexInfo(config, indexName)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
} else {
|
|
||||||
checkEqual(t, index.Name, expectedIndexInfo.Name, indexName+" name")
|
|
||||||
checkEqual(t, index.Official, expectedIndexInfo.Official, indexName+" is official")
|
|
||||||
checkEqual(t, index.Secure, expectedIndexInfo.Secure, indexName+" is secure")
|
|
||||||
checkEqual(t, len(index.Mirrors), len(expectedIndexInfo.Mirrors), indexName+" mirrors")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
config := newServiceConfig(ServiceOptions{})
|
|
||||||
noMirrors := []string{}
|
|
||||||
expectedIndexInfos := map[string]*registrytypes.IndexInfo{
|
|
||||||
IndexName: {
|
|
||||||
Name: IndexName,
|
|
||||||
Official: true,
|
|
||||||
Secure: true,
|
|
||||||
Mirrors: noMirrors,
|
|
||||||
},
|
|
||||||
"index." + IndexName: {
|
|
||||||
Name: IndexName,
|
|
||||||
Official: true,
|
|
||||||
Secure: true,
|
|
||||||
Mirrors: noMirrors,
|
|
||||||
},
|
|
||||||
"example.com": {
|
|
||||||
Name: "example.com",
|
|
||||||
Official: false,
|
|
||||||
Secure: true,
|
|
||||||
Mirrors: noMirrors,
|
|
||||||
},
|
|
||||||
"127.0.0.1:5000": {
|
|
||||||
Name: "127.0.0.1:5000",
|
|
||||||
Official: false,
|
|
||||||
Secure: false,
|
|
||||||
Mirrors: noMirrors,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
testIndexInfo(config, expectedIndexInfos)
|
|
||||||
|
|
||||||
publicMirrors := []string{"http://mirror1.local", "http://mirror2.local"}
|
|
||||||
config = makeServiceConfig(publicMirrors, []string{"example.com"})
|
|
||||||
|
|
||||||
expectedIndexInfos = map[string]*registrytypes.IndexInfo{
|
|
||||||
IndexName: {
|
|
||||||
Name: IndexName,
|
|
||||||
Official: true,
|
|
||||||
Secure: true,
|
|
||||||
Mirrors: publicMirrors,
|
|
||||||
},
|
|
||||||
"index." + IndexName: {
|
|
||||||
Name: IndexName,
|
|
||||||
Official: true,
|
|
||||||
Secure: true,
|
|
||||||
Mirrors: publicMirrors,
|
|
||||||
},
|
|
||||||
"example.com": {
|
|
||||||
Name: "example.com",
|
|
||||||
Official: false,
|
|
||||||
Secure: false,
|
|
||||||
Mirrors: noMirrors,
|
|
||||||
},
|
|
||||||
"example.com:5000": {
|
|
||||||
Name: "example.com:5000",
|
|
||||||
Official: false,
|
|
||||||
Secure: true,
|
|
||||||
Mirrors: noMirrors,
|
|
||||||
},
|
|
||||||
"127.0.0.1": {
|
|
||||||
Name: "127.0.0.1",
|
|
||||||
Official: false,
|
|
||||||
Secure: false,
|
|
||||||
Mirrors: noMirrors,
|
|
||||||
},
|
|
||||||
"127.0.0.1:5000": {
|
|
||||||
Name: "127.0.0.1:5000",
|
|
||||||
Official: false,
|
|
||||||
Secure: false,
|
|
||||||
Mirrors: noMirrors,
|
|
||||||
},
|
|
||||||
"other.com": {
|
|
||||||
Name: "other.com",
|
|
||||||
Official: false,
|
|
||||||
Secure: true,
|
|
||||||
Mirrors: noMirrors,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
testIndexInfo(config, expectedIndexInfos)
|
|
||||||
|
|
||||||
config = makeServiceConfig(nil, []string{"42.42.0.0/16"})
|
|
||||||
expectedIndexInfos = map[string]*registrytypes.IndexInfo{
|
|
||||||
"example.com": {
|
|
||||||
Name: "example.com",
|
|
||||||
Official: false,
|
|
||||||
Secure: false,
|
|
||||||
Mirrors: noMirrors,
|
|
||||||
},
|
|
||||||
"example.com:5000": {
|
|
||||||
Name: "example.com:5000",
|
|
||||||
Official: false,
|
|
||||||
Secure: false,
|
|
||||||
Mirrors: noMirrors,
|
|
||||||
},
|
|
||||||
"127.0.0.1": {
|
|
||||||
Name: "127.0.0.1",
|
|
||||||
Official: false,
|
|
||||||
Secure: false,
|
|
||||||
Mirrors: noMirrors,
|
|
||||||
},
|
|
||||||
"127.0.0.1:5000": {
|
|
||||||
Name: "127.0.0.1:5000",
|
|
||||||
Official: false,
|
|
||||||
Secure: false,
|
|
||||||
Mirrors: noMirrors,
|
|
||||||
},
|
|
||||||
"other.com": {
|
|
||||||
Name: "other.com",
|
|
||||||
Official: false,
|
|
||||||
Secure: true,
|
|
||||||
Mirrors: noMirrors,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
testIndexInfo(config, expectedIndexInfos)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMirrorEndpointLookup(t *testing.T) {
|
|
||||||
containsMirror := func(endpoints []APIEndpoint) bool {
|
|
||||||
for _, pe := range endpoints {
|
|
||||||
if pe.URL.Host == "my.mirror" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
s := DefaultService{config: makeServiceConfig([]string{"my.mirror"}, nil)}
|
|
||||||
|
|
||||||
imageName, err := reference.WithName(IndexName + "/test/image")
|
|
||||||
if err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
pushAPIEndpoints, err := s.LookupPushEndpoints(imageName.Hostname())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if containsMirror(pushAPIEndpoints) {
|
|
||||||
t.Fatal("Push endpoint should not contain mirror")
|
|
||||||
}
|
|
||||||
|
|
||||||
pullAPIEndpoints, err := s.LookupPullEndpoints(imageName.Hostname())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if !containsMirror(pullAPIEndpoints) {
|
|
||||||
t.Fatal("Pull endpoint should contain mirror")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPushRegistryTag(t *testing.T) {
|
|
||||||
r := spawnTestRegistrySession(t)
|
|
||||||
repoRef, err := reference.ParseNamed(REPO)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
err = r.PushRegistryTag(repoRef, imageID, "stable", makeURL("/v1/"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPushImageJSONIndex(t *testing.T) {
|
|
||||||
r := spawnTestRegistrySession(t)
|
|
||||||
imgData := []*ImgData{
|
|
||||||
{
|
|
||||||
ID: "77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20",
|
|
||||||
Checksum: "sha256:1ac330d56e05eef6d438586545ceff7550d3bdcb6b19961f12c5ba714ee1bb37",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ID: "42d718c941f5c532ac049bf0b0ab53f0062f09a03afd4aa4a02c098e46032b9d",
|
|
||||||
Checksum: "sha256:bea7bf2e4bacd479344b737328db47b18880d09096e6674165533aa994f5e9f2",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
repoRef, err := reference.ParseNamed(REPO)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
repoData, err := r.PushImageJSONIndex(repoRef, imgData, false, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if repoData == nil {
|
|
||||||
t.Fatal("Expected RepositoryData object")
|
|
||||||
}
|
|
||||||
repoData, err = r.PushImageJSONIndex(repoRef, imgData, true, []string{r.indexEndpoint.String()})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if repoData == nil {
|
|
||||||
t.Fatal("Expected RepositoryData object")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestSearchRepositories(t *testing.T) {
|
|
||||||
r := spawnTestRegistrySession(t)
|
|
||||||
results, err := r.SearchRepositories("fakequery", 25)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if results == nil {
|
|
||||||
t.Fatal("Expected non-nil SearchResults object")
|
|
||||||
}
|
|
||||||
assertEqual(t, results.NumResults, 1, "Expected 1 search results")
|
|
||||||
assertEqual(t, results.Query, "fakequery", "Expected 'fakequery' as query")
|
|
||||||
assertEqual(t, results.Results[0].StarCount, 42, "Expected 'fakeimage' to have 42 stars")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTrustedLocation(t *testing.T) {
|
|
||||||
for _, url := range []string{"http://example.com", "https://example.com:7777", "http://docker.io", "http://test.docker.com", "https://fakedocker.com"} {
|
|
||||||
req, _ := http.NewRequest("GET", url, nil)
|
|
||||||
if trustedLocation(req) == true {
|
|
||||||
t.Fatalf("'%s' shouldn't be detected as a trusted location", url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, url := range []string{"https://docker.io", "https://test.docker.com:80"} {
|
|
||||||
req, _ := http.NewRequest("GET", url, nil)
|
|
||||||
if trustedLocation(req) == false {
|
|
||||||
t.Fatalf("'%s' should be detected as a trusted location", url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAddRequiredHeadersToRedirectedRequests(t *testing.T) {
|
|
||||||
for _, urls := range [][]string{
|
|
||||||
{"http://docker.io", "https://docker.com"},
|
|
||||||
{"https://foo.docker.io:7777", "http://bar.docker.com"},
|
|
||||||
{"https://foo.docker.io", "https://example.com"},
|
|
||||||
} {
|
|
||||||
reqFrom, _ := http.NewRequest("GET", urls[0], nil)
|
|
||||||
reqFrom.Header.Add("Content-Type", "application/json")
|
|
||||||
reqFrom.Header.Add("Authorization", "super_secret")
|
|
||||||
reqTo, _ := http.NewRequest("GET", urls[1], nil)
|
|
||||||
|
|
||||||
addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom})
|
|
||||||
|
|
||||||
if len(reqTo.Header) != 1 {
|
|
||||||
t.Fatalf("Expected 1 headers, got %d", len(reqTo.Header))
|
|
||||||
}
|
|
||||||
|
|
||||||
if reqTo.Header.Get("Content-Type") != "application/json" {
|
|
||||||
t.Fatal("'Content-Type' should be 'application/json'")
|
|
||||||
}
|
|
||||||
|
|
||||||
if reqTo.Header.Get("Authorization") != "" {
|
|
||||||
t.Fatal("'Authorization' should be empty")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, urls := range [][]string{
|
|
||||||
{"https://docker.io", "https://docker.com"},
|
|
||||||
{"https://foo.docker.io:7777", "https://bar.docker.com"},
|
|
||||||
} {
|
|
||||||
reqFrom, _ := http.NewRequest("GET", urls[0], nil)
|
|
||||||
reqFrom.Header.Add("Content-Type", "application/json")
|
|
||||||
reqFrom.Header.Add("Authorization", "super_secret")
|
|
||||||
reqTo, _ := http.NewRequest("GET", urls[1], nil)
|
|
||||||
|
|
||||||
addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom})
|
|
||||||
|
|
||||||
if len(reqTo.Header) != 2 {
|
|
||||||
t.Fatalf("Expected 2 headers, got %d", len(reqTo.Header))
|
|
||||||
}
|
|
||||||
|
|
||||||
if reqTo.Header.Get("Content-Type") != "application/json" {
|
|
||||||
t.Fatal("'Content-Type' should be 'application/json'")
|
|
||||||
}
|
|
||||||
|
|
||||||
if reqTo.Header.Get("Authorization") != "super_secret" {
|
|
||||||
t.Fatal("'Authorization' should be 'super_secret'")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIsSecureIndex(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
addr string
|
|
||||||
insecureRegistries []string
|
|
||||||
expected bool
|
|
||||||
}{
|
|
||||||
{IndexName, nil, true},
|
|
||||||
{"example.com", []string{}, true},
|
|
||||||
{"example.com", []string{"example.com"}, false},
|
|
||||||
{"localhost", []string{"localhost:5000"}, false},
|
|
||||||
{"localhost:5000", []string{"localhost:5000"}, false},
|
|
||||||
{"localhost", []string{"example.com"}, false},
|
|
||||||
{"127.0.0.1:5000", []string{"127.0.0.1:5000"}, false},
|
|
||||||
{"localhost", nil, false},
|
|
||||||
{"localhost:5000", nil, false},
|
|
||||||
{"127.0.0.1", nil, false},
|
|
||||||
{"localhost", []string{"example.com"}, false},
|
|
||||||
{"127.0.0.1", []string{"example.com"}, false},
|
|
||||||
{"example.com", nil, true},
|
|
||||||
{"example.com", []string{"example.com"}, false},
|
|
||||||
{"127.0.0.1", []string{"example.com"}, false},
|
|
||||||
{"127.0.0.1:5000", []string{"example.com"}, false},
|
|
||||||
{"example.com:5000", []string{"42.42.0.0/16"}, false},
|
|
||||||
{"example.com", []string{"42.42.0.0/16"}, false},
|
|
||||||
{"example.com:5000", []string{"42.42.42.42/8"}, false},
|
|
||||||
{"127.0.0.1:5000", []string{"127.0.0.0/8"}, false},
|
|
||||||
{"42.42.42.42:5000", []string{"42.1.1.1/8"}, false},
|
|
||||||
{"invalid.domain.com", []string{"42.42.0.0/16"}, true},
|
|
||||||
{"invalid.domain.com", []string{"invalid.domain.com"}, false},
|
|
||||||
{"invalid.domain.com:5000", []string{"invalid.domain.com"}, true},
|
|
||||||
{"invalid.domain.com:5000", []string{"invalid.domain.com:5000"}, false},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
config := makeServiceConfig(nil, tt.insecureRegistries)
|
|
||||||
if sec := isSecureIndex(config, tt.addr); sec != tt.expected {
|
|
||||||
t.Errorf("isSecureIndex failed for %q %v, expected %v got %v", tt.addr, tt.insecureRegistries, tt.expected, sec)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type debugTransport struct {
|
|
||||||
http.RoundTripper
|
|
||||||
log func(...interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tr debugTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
|
||||||
dump, err := httputil.DumpRequestOut(req, false)
|
|
||||||
if err != nil {
|
|
||||||
tr.log("could not dump request")
|
|
||||||
}
|
|
||||||
tr.log(string(dump))
|
|
||||||
resp, err := tr.RoundTripper.RoundTrip(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
dump, err = httputil.DumpResponse(resp, false)
|
|
||||||
if err != nil {
|
|
||||||
tr.log("could not dump response")
|
|
||||||
}
|
|
||||||
tr.log(string(dump))
|
|
||||||
return resp, err
|
|
||||||
}
|
|
260
docs/service.go
260
docs/service.go
|
@ -1,260 +0,0 @@
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/net/context"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/docker/distribution/registry/client/auth"
|
|
||||||
"github.com/docker/docker/reference"
|
|
||||||
"github.com/docker/engine-api/types"
|
|
||||||
registrytypes "github.com/docker/engine-api/types/registry"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// DefaultSearchLimit is the default value for maximum number of returned search results.
|
|
||||||
DefaultSearchLimit = 25
|
|
||||||
)
|
|
||||||
|
|
||||||
// Service is the interface defining what a registry service should implement.
|
|
||||||
type Service interface {
|
|
||||||
Auth(ctx context.Context, authConfig *types.AuthConfig, userAgent string) (status, token string, err error)
|
|
||||||
LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error)
|
|
||||||
LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error)
|
|
||||||
ResolveRepository(name reference.Named) (*RepositoryInfo, error)
|
|
||||||
ResolveIndex(name string) (*registrytypes.IndexInfo, error)
|
|
||||||
Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error)
|
|
||||||
ServiceConfig() *registrytypes.ServiceConfig
|
|
||||||
TLSConfig(hostname string) (*tls.Config, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultService is a registry service. It tracks configuration data such as a list
|
|
||||||
// of mirrors.
|
|
||||||
type DefaultService struct {
|
|
||||||
config *serviceConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewService returns a new instance of DefaultService ready to be
|
|
||||||
// installed into an engine.
|
|
||||||
func NewService(options ServiceOptions) *DefaultService {
|
|
||||||
return &DefaultService{
|
|
||||||
config: newServiceConfig(options),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceConfig returns the public registry service configuration.
|
|
||||||
func (s *DefaultService) ServiceConfig() *registrytypes.ServiceConfig {
|
|
||||||
return &s.config.ServiceConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auth contacts the public registry with the provided credentials,
|
|
||||||
// and returns OK if authentication was successful.
|
|
||||||
// It can be used to verify the validity of a client's credentials.
|
|
||||||
func (s *DefaultService) Auth(ctx context.Context, authConfig *types.AuthConfig, userAgent string) (status, token string, err error) {
|
|
||||||
// TODO Use ctx when searching for repositories
|
|
||||||
serverAddress := authConfig.ServerAddress
|
|
||||||
if serverAddress == "" {
|
|
||||||
serverAddress = IndexServer
|
|
||||||
}
|
|
||||||
if !strings.HasPrefix(serverAddress, "https://") && !strings.HasPrefix(serverAddress, "http://") {
|
|
||||||
serverAddress = "https://" + serverAddress
|
|
||||||
}
|
|
||||||
u, err := url.Parse(serverAddress)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", fmt.Errorf("unable to parse server address: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoints, err := s.LookupPushEndpoints(u.Host)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, endpoint := range endpoints {
|
|
||||||
login := loginV2
|
|
||||||
if endpoint.Version == APIVersion1 {
|
|
||||||
login = loginV1
|
|
||||||
}
|
|
||||||
|
|
||||||
status, token, err = login(authConfig, endpoint, userAgent)
|
|
||||||
if err == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if fErr, ok := err.(fallbackError); ok {
|
|
||||||
err = fErr.err
|
|
||||||
logrus.Infof("Error logging in to %s endpoint, trying next endpoint: %v", endpoint.Version, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// splitReposSearchTerm breaks a search term into an index name and remote name
|
|
||||||
func splitReposSearchTerm(reposName string) (string, string) {
|
|
||||||
nameParts := strings.SplitN(reposName, "/", 2)
|
|
||||||
var indexName, remoteName string
|
|
||||||
if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") &&
|
|
||||||
!strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") {
|
|
||||||
// This is a Docker Index repos (ex: samalba/hipache or ubuntu)
|
|
||||||
// 'docker.io'
|
|
||||||
indexName = IndexName
|
|
||||||
remoteName = reposName
|
|
||||||
} else {
|
|
||||||
indexName = nameParts[0]
|
|
||||||
remoteName = nameParts[1]
|
|
||||||
}
|
|
||||||
return indexName, remoteName
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search queries the public registry for images matching the specified
|
|
||||||
// search terms, and returns the results.
|
|
||||||
func (s *DefaultService) Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) {
|
|
||||||
// TODO Use ctx when searching for repositories
|
|
||||||
if err := validateNoScheme(term); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
indexName, remoteName := splitReposSearchTerm(term)
|
|
||||||
|
|
||||||
index, err := newIndexInfo(s.config, indexName)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// *TODO: Search multiple indexes.
|
|
||||||
endpoint, err := NewV1Endpoint(index, userAgent, http.Header(headers))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var client *http.Client
|
|
||||||
if authConfig != nil && authConfig.IdentityToken != "" && authConfig.Username != "" {
|
|
||||||
creds := NewStaticCredentialStore(authConfig)
|
|
||||||
scopes := []auth.Scope{
|
|
||||||
auth.RegistryScope{
|
|
||||||
Name: "catalog",
|
|
||||||
Actions: []string{"search"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
modifiers := DockerHeaders(userAgent, nil)
|
|
||||||
v2Client, foundV2, err := v2AuthHTTPClient(endpoint.URL, endpoint.client.Transport, modifiers, creds, scopes)
|
|
||||||
if err != nil {
|
|
||||||
if fErr, ok := err.(fallbackError); ok {
|
|
||||||
logrus.Errorf("Cannot use identity token for search, v2 auth not supported: %v", fErr.err)
|
|
||||||
} else {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else if foundV2 {
|
|
||||||
// Copy non transport http client features
|
|
||||||
v2Client.Timeout = endpoint.client.Timeout
|
|
||||||
v2Client.CheckRedirect = endpoint.client.CheckRedirect
|
|
||||||
v2Client.Jar = endpoint.client.Jar
|
|
||||||
|
|
||||||
logrus.Debugf("using v2 client for search to %s", endpoint.URL)
|
|
||||||
client = v2Client
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if client == nil {
|
|
||||||
client = endpoint.client
|
|
||||||
if err := authorizeClient(client, authConfig, endpoint); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
r := newSession(client, authConfig, endpoint)
|
|
||||||
|
|
||||||
if index.Official {
|
|
||||||
localName := remoteName
|
|
||||||
if strings.HasPrefix(localName, "library/") {
|
|
||||||
// If pull "library/foo", it's stored locally under "foo"
|
|
||||||
localName = strings.SplitN(localName, "/", 2)[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.SearchRepositories(localName, limit)
|
|
||||||
}
|
|
||||||
return r.SearchRepositories(remoteName, limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResolveRepository splits a repository name into its components
|
|
||||||
// and configuration of the associated registry.
|
|
||||||
func (s *DefaultService) ResolveRepository(name reference.Named) (*RepositoryInfo, error) {
|
|
||||||
return newRepositoryInfo(s.config, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResolveIndex takes indexName and returns index info
|
|
||||||
func (s *DefaultService) ResolveIndex(name string) (*registrytypes.IndexInfo, error) {
|
|
||||||
return newIndexInfo(s.config, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// APIEndpoint represents a remote API endpoint
|
|
||||||
type APIEndpoint struct {
|
|
||||||
Mirror bool
|
|
||||||
URL *url.URL
|
|
||||||
Version APIVersion
|
|
||||||
Official bool
|
|
||||||
TrimHostname bool
|
|
||||||
TLSConfig *tls.Config
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToV1Endpoint returns a V1 API endpoint based on the APIEndpoint
|
|
||||||
func (e APIEndpoint) ToV1Endpoint(userAgent string, metaHeaders http.Header) (*V1Endpoint, error) {
|
|
||||||
return newV1Endpoint(*e.URL, e.TLSConfig, userAgent, metaHeaders)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TLSConfig constructs a client TLS configuration based on server defaults
|
|
||||||
func (s *DefaultService) TLSConfig(hostname string) (*tls.Config, error) {
|
|
||||||
return newTLSConfig(hostname, isSecureIndex(s.config, hostname))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *DefaultService) tlsConfigForMirror(mirrorURL *url.URL) (*tls.Config, error) {
|
|
||||||
return s.TLSConfig(mirrorURL.Host)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LookupPullEndpoints creates a list of endpoints to try to pull from, in order of preference.
|
|
||||||
// It gives preference to v2 endpoints over v1, mirrors over the actual
|
|
||||||
// registry, and HTTPS over plain HTTP.
|
|
||||||
func (s *DefaultService) LookupPullEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
|
|
||||||
return s.lookupEndpoints(hostname)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LookupPushEndpoints creates a list of endpoints to try to push to, in order of preference.
|
|
||||||
// It gives preference to v2 endpoints over v1, and HTTPS over plain HTTP.
|
|
||||||
// Mirrors are not included.
|
|
||||||
func (s *DefaultService) LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
|
|
||||||
allEndpoints, err := s.lookupEndpoints(hostname)
|
|
||||||
if err == nil {
|
|
||||||
for _, endpoint := range allEndpoints {
|
|
||||||
if !endpoint.Mirror {
|
|
||||||
endpoints = append(endpoints, endpoint)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return endpoints, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *DefaultService) lookupEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
|
|
||||||
endpoints, err = s.lookupV2Endpoints(hostname)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.config.V2Only {
|
|
||||||
return endpoints, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
legacyEndpoints, err := s.lookupV1Endpoints(hostname)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
endpoints = append(endpoints, legacyEndpoints...)
|
|
||||||
|
|
||||||
return endpoints, nil
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/docker/go-connections/tlsconfig"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *DefaultService) lookupV1Endpoints(hostname string) (endpoints []APIEndpoint, err error) {
|
|
||||||
var cfg = tlsconfig.ServerDefault
|
|
||||||
tlsConfig := &cfg
|
|
||||||
if hostname == DefaultNamespace {
|
|
||||||
endpoints = append(endpoints, APIEndpoint{
|
|
||||||
URL: DefaultV1Registry,
|
|
||||||
Version: APIVersion1,
|
|
||||||
Official: true,
|
|
||||||
TrimHostname: true,
|
|
||||||
TLSConfig: tlsConfig,
|
|
||||||
})
|
|
||||||
return endpoints, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsConfig, err = s.TLSConfig(hostname)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoints = []APIEndpoint{
|
|
||||||
{
|
|
||||||
URL: &url.URL{
|
|
||||||
Scheme: "https",
|
|
||||||
Host: hostname,
|
|
||||||
},
|
|
||||||
Version: APIVersion1,
|
|
||||||
TrimHostname: true,
|
|
||||||
TLSConfig: tlsConfig,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if tlsConfig.InsecureSkipVerify {
|
|
||||||
endpoints = append(endpoints, APIEndpoint{ // or this
|
|
||||||
URL: &url.URL{
|
|
||||||
Scheme: "http",
|
|
||||||
Host: hostname,
|
|
||||||
},
|
|
||||||
Version: APIVersion1,
|
|
||||||
TrimHostname: true,
|
|
||||||
// used to check if supposed to be secure via InsecureSkipVerify
|
|
||||||
TLSConfig: tlsConfig,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return endpoints, nil
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/docker/go-connections/tlsconfig"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *DefaultService) lookupV2Endpoints(hostname string) (endpoints []APIEndpoint, err error) {
|
|
||||||
var cfg = tlsconfig.ServerDefault
|
|
||||||
tlsConfig := &cfg
|
|
||||||
if hostname == DefaultNamespace || hostname == DefaultV1Registry.Host {
|
|
||||||
// v2 mirrors
|
|
||||||
for _, mirror := range s.config.Mirrors {
|
|
||||||
if !strings.HasPrefix(mirror, "http://") && !strings.HasPrefix(mirror, "https://") {
|
|
||||||
mirror = "https://" + mirror
|
|
||||||
}
|
|
||||||
mirrorURL, err := url.Parse(mirror)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
mirrorTLSConfig, err := s.tlsConfigForMirror(mirrorURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
endpoints = append(endpoints, APIEndpoint{
|
|
||||||
URL: mirrorURL,
|
|
||||||
// guess mirrors are v2
|
|
||||||
Version: APIVersion2,
|
|
||||||
Mirror: true,
|
|
||||||
TrimHostname: true,
|
|
||||||
TLSConfig: mirrorTLSConfig,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// v2 registry
|
|
||||||
endpoints = append(endpoints, APIEndpoint{
|
|
||||||
URL: DefaultV2Registry,
|
|
||||||
Version: APIVersion2,
|
|
||||||
Official: true,
|
|
||||||
TrimHostname: true,
|
|
||||||
TLSConfig: tlsConfig,
|
|
||||||
})
|
|
||||||
|
|
||||||
return endpoints, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsConfig, err = s.TLSConfig(hostname)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoints = []APIEndpoint{
|
|
||||||
{
|
|
||||||
URL: &url.URL{
|
|
||||||
Scheme: "https",
|
|
||||||
Host: hostname,
|
|
||||||
},
|
|
||||||
Version: APIVersion2,
|
|
||||||
TrimHostname: true,
|
|
||||||
TLSConfig: tlsConfig,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if tlsConfig.InsecureSkipVerify {
|
|
||||||
endpoints = append(endpoints, APIEndpoint{
|
|
||||||
URL: &url.URL{
|
|
||||||
Scheme: "http",
|
|
||||||
Host: hostname,
|
|
||||||
},
|
|
||||||
Version: APIVersion2,
|
|
||||||
TrimHostname: true,
|
|
||||||
// used to check if supposed to be secure via InsecureSkipVerify
|
|
||||||
TLSConfig: tlsConfig,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return endpoints, nil
|
|
||||||
}
|
|
783
docs/session.go
783
docs/session.go
|
@ -1,783 +0,0 @@
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/sha256"
|
|
||||||
"errors"
|
|
||||||
"sync"
|
|
||||||
// this is required for some certificates
|
|
||||||
_ "crypto/sha512"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/http/cookiejar"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/Sirupsen/logrus"
|
|
||||||
"github.com/docker/distribution/registry/api/errcode"
|
|
||||||
"github.com/docker/docker/pkg/httputils"
|
|
||||||
"github.com/docker/docker/pkg/ioutils"
|
|
||||||
"github.com/docker/docker/pkg/stringid"
|
|
||||||
"github.com/docker/docker/pkg/tarsum"
|
|
||||||
"github.com/docker/docker/reference"
|
|
||||||
"github.com/docker/engine-api/types"
|
|
||||||
registrytypes "github.com/docker/engine-api/types/registry"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// ErrRepoNotFound is returned if the repository didn't exist on the
|
|
||||||
// remote side
|
|
||||||
ErrRepoNotFound = errors.New("Repository not found")
|
|
||||||
)
|
|
||||||
|
|
||||||
// A Session is used to communicate with a V1 registry
|
|
||||||
type Session struct {
|
|
||||||
indexEndpoint *V1Endpoint
|
|
||||||
client *http.Client
|
|
||||||
// TODO(tiborvass): remove authConfig
|
|
||||||
authConfig *types.AuthConfig
|
|
||||||
id string
|
|
||||||
}
|
|
||||||
|
|
||||||
type authTransport struct {
|
|
||||||
http.RoundTripper
|
|
||||||
*types.AuthConfig
|
|
||||||
|
|
||||||
alwaysSetBasicAuth bool
|
|
||||||
token []string
|
|
||||||
|
|
||||||
mu sync.Mutex // guards modReq
|
|
||||||
modReq map[*http.Request]*http.Request // original -> modified
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthTransport handles the auth layer when communicating with a v1 registry (private or official)
|
|
||||||
//
|
|
||||||
// For private v1 registries, set alwaysSetBasicAuth to true.
|
|
||||||
//
|
|
||||||
// For the official v1 registry, if there isn't already an Authorization header in the request,
|
|
||||||
// but there is an X-Docker-Token header set to true, then Basic Auth will be used to set the Authorization header.
|
|
||||||
// After sending the request with the provided base http.RoundTripper, if an X-Docker-Token header, representing
|
|
||||||
// a token, is present in the response, then it gets cached and sent in the Authorization header of all subsequent
|
|
||||||
// requests.
|
|
||||||
//
|
|
||||||
// If the server sends a token without the client having requested it, it is ignored.
|
|
||||||
//
|
|
||||||
// This RoundTripper also has a CancelRequest method important for correct timeout handling.
|
|
||||||
func AuthTransport(base http.RoundTripper, authConfig *types.AuthConfig, alwaysSetBasicAuth bool) http.RoundTripper {
|
|
||||||
if base == nil {
|
|
||||||
base = http.DefaultTransport
|
|
||||||
}
|
|
||||||
return &authTransport{
|
|
||||||
RoundTripper: base,
|
|
||||||
AuthConfig: authConfig,
|
|
||||||
alwaysSetBasicAuth: alwaysSetBasicAuth,
|
|
||||||
modReq: make(map[*http.Request]*http.Request),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// cloneRequest returns a clone of the provided *http.Request.
|
|
||||||
// The clone is a shallow copy of the struct and its Header map.
|
|
||||||
func cloneRequest(r *http.Request) *http.Request {
|
|
||||||
// shallow copy of the struct
|
|
||||||
r2 := new(http.Request)
|
|
||||||
*r2 = *r
|
|
||||||
// deep copy of the Header
|
|
||||||
r2.Header = make(http.Header, len(r.Header))
|
|
||||||
for k, s := range r.Header {
|
|
||||||
r2.Header[k] = append([]string(nil), s...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r2
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoundTrip changes an HTTP request's headers to add the necessary
|
|
||||||
// authentication-related headers
|
|
||||||
func (tr *authTransport) RoundTrip(orig *http.Request) (*http.Response, error) {
|
|
||||||
// Authorization should not be set on 302 redirect for untrusted locations.
|
|
||||||
// This logic mirrors the behavior in addRequiredHeadersToRedirectedRequests.
|
|
||||||
// As the authorization logic is currently implemented in RoundTrip,
|
|
||||||
// a 302 redirect is detected by looking at the Referrer header as go http package adds said header.
|
|
||||||
// This is safe as Docker doesn't set Referrer in other scenarios.
|
|
||||||
if orig.Header.Get("Referer") != "" && !trustedLocation(orig) {
|
|
||||||
return tr.RoundTripper.RoundTrip(orig)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := cloneRequest(orig)
|
|
||||||
tr.mu.Lock()
|
|
||||||
tr.modReq[orig] = req
|
|
||||||
tr.mu.Unlock()
|
|
||||||
|
|
||||||
if tr.alwaysSetBasicAuth {
|
|
||||||
if tr.AuthConfig == nil {
|
|
||||||
return nil, errors.New("unexpected error: empty auth config")
|
|
||||||
}
|
|
||||||
req.SetBasicAuth(tr.Username, tr.Password)
|
|
||||||
return tr.RoundTripper.RoundTrip(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't override
|
|
||||||
if req.Header.Get("Authorization") == "" {
|
|
||||||
if req.Header.Get("X-Docker-Token") == "true" && tr.AuthConfig != nil && len(tr.Username) > 0 {
|
|
||||||
req.SetBasicAuth(tr.Username, tr.Password)
|
|
||||||
} else if len(tr.token) > 0 {
|
|
||||||
req.Header.Set("Authorization", "Token "+strings.Join(tr.token, ","))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resp, err := tr.RoundTripper.RoundTrip(req)
|
|
||||||
if err != nil {
|
|
||||||
delete(tr.modReq, orig)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(resp.Header["X-Docker-Token"]) > 0 {
|
|
||||||
tr.token = resp.Header["X-Docker-Token"]
|
|
||||||
}
|
|
||||||
resp.Body = &ioutils.OnEOFReader{
|
|
||||||
Rc: resp.Body,
|
|
||||||
Fn: func() {
|
|
||||||
tr.mu.Lock()
|
|
||||||
delete(tr.modReq, orig)
|
|
||||||
tr.mu.Unlock()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CancelRequest cancels an in-flight request by closing its connection.
|
|
||||||
func (tr *authTransport) CancelRequest(req *http.Request) {
|
|
||||||
type canceler interface {
|
|
||||||
CancelRequest(*http.Request)
|
|
||||||
}
|
|
||||||
if cr, ok := tr.RoundTripper.(canceler); ok {
|
|
||||||
tr.mu.Lock()
|
|
||||||
modReq := tr.modReq[req]
|
|
||||||
delete(tr.modReq, req)
|
|
||||||
tr.mu.Unlock()
|
|
||||||
cr.CancelRequest(modReq)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func authorizeClient(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) error {
|
|
||||||
var alwaysSetBasicAuth bool
|
|
||||||
|
|
||||||
// If we're working with a standalone private registry over HTTPS, send Basic Auth headers
|
|
||||||
// alongside all our requests.
|
|
||||||
if endpoint.String() != IndexServer && endpoint.URL.Scheme == "https" {
|
|
||||||
info, err := endpoint.Ping()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if info.Standalone && authConfig != nil {
|
|
||||||
logrus.Debugf("Endpoint %s is eligible for private registry. Enabling decorator.", endpoint.String())
|
|
||||||
alwaysSetBasicAuth = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Annotate the transport unconditionally so that v2 can
|
|
||||||
// properly fallback on v1 when an image is not found.
|
|
||||||
client.Transport = AuthTransport(client.Transport, authConfig, alwaysSetBasicAuth)
|
|
||||||
|
|
||||||
jar, err := cookiejar.New(nil)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New("cookiejar.New is not supposed to return an error")
|
|
||||||
}
|
|
||||||
client.Jar = jar
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) *Session {
|
|
||||||
return &Session{
|
|
||||||
authConfig: authConfig,
|
|
||||||
client: client,
|
|
||||||
indexEndpoint: endpoint,
|
|
||||||
id: stringid.GenerateRandomID(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSession creates a new session
|
|
||||||
// TODO(tiborvass): remove authConfig param once registry client v2 is vendored
|
|
||||||
func NewSession(client *http.Client, authConfig *types.AuthConfig, endpoint *V1Endpoint) (*Session, error) {
|
|
||||||
if err := authorizeClient(client, authConfig, endpoint); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return newSession(client, authConfig, endpoint), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ID returns this registry session's ID.
|
|
||||||
func (r *Session) ID() string {
|
|
||||||
return r.id
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRemoteHistory retrieves the history of a given image from the registry.
|
|
||||||
// It returns a list of the parent's JSON files (including the requested image).
|
|
||||||
func (r *Session) GetRemoteHistory(imgID, registry string) ([]string, error) {
|
|
||||||
res, err := r.client.Get(registry + "images/" + imgID + "/ancestry")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
if res.StatusCode == 401 {
|
|
||||||
return nil, errcode.ErrorCodeUnauthorized.WithArgs()
|
|
||||||
}
|
|
||||||
return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Server error: %d trying to fetch remote history for %s", res.StatusCode, imgID), res)
|
|
||||||
}
|
|
||||||
|
|
||||||
var history []string
|
|
||||||
if err := json.NewDecoder(res.Body).Decode(&history); err != nil {
|
|
||||||
return nil, fmt.Errorf("Error while reading the http response: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("Ancestry: %v", history)
|
|
||||||
return history, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LookupRemoteImage checks if an image exists in the registry
|
|
||||||
func (r *Session) LookupRemoteImage(imgID, registry string) error {
|
|
||||||
res, err := r.client.Get(registry + "images/" + imgID + "/json")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
res.Body.Close()
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
return httputils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d", res.StatusCode), res)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRemoteImageJSON retrieves an image's JSON metadata from the registry.
|
|
||||||
func (r *Session) GetRemoteImageJSON(imgID, registry string) ([]byte, int64, error) {
|
|
||||||
res, err := r.client.Get(registry + "images/" + imgID + "/json")
|
|
||||||
if err != nil {
|
|
||||||
return nil, -1, fmt.Errorf("Failed to download json: %s", err)
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
return nil, -1, httputils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d", res.StatusCode), res)
|
|
||||||
}
|
|
||||||
// if the size header is not present, then set it to '-1'
|
|
||||||
imageSize := int64(-1)
|
|
||||||
if hdr := res.Header.Get("X-Docker-Size"); hdr != "" {
|
|
||||||
imageSize, err = strconv.ParseInt(hdr, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return nil, -1, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonString, err := ioutil.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, -1, fmt.Errorf("Failed to parse downloaded json: %v (%s)", err, jsonString)
|
|
||||||
}
|
|
||||||
return jsonString, imageSize, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRemoteImageLayer retrieves an image layer from the registry
|
|
||||||
func (r *Session) GetRemoteImageLayer(imgID, registry string, imgSize int64) (io.ReadCloser, error) {
|
|
||||||
var (
|
|
||||||
statusCode = 0
|
|
||||||
res *http.Response
|
|
||||||
err error
|
|
||||||
imageURL = fmt.Sprintf("%simages/%s/layer", registry, imgID)
|
|
||||||
)
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", imageURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Error while getting from the server: %v", err)
|
|
||||||
}
|
|
||||||
statusCode = 0
|
|
||||||
res, err = r.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Debugf("Error contacting registry %s: %v", registry, err)
|
|
||||||
// the only case err != nil && res != nil is https://golang.org/src/net/http/client.go#L515
|
|
||||||
if res != nil {
|
|
||||||
if res.Body != nil {
|
|
||||||
res.Body.Close()
|
|
||||||
}
|
|
||||||
statusCode = res.StatusCode
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("Server error: Status %d while fetching image layer (%s)",
|
|
||||||
statusCode, imgID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
res.Body.Close()
|
|
||||||
return nil, fmt.Errorf("Server error: Status %d while fetching image layer (%s)",
|
|
||||||
res.StatusCode, imgID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if res.Header.Get("Accept-Ranges") == "bytes" && imgSize > 0 {
|
|
||||||
logrus.Debug("server supports resume")
|
|
||||||
return httputils.ResumableRequestReaderWithInitialResponse(r.client, req, 5, imgSize, res), nil
|
|
||||||
}
|
|
||||||
logrus.Debug("server doesn't support resume")
|
|
||||||
return res.Body, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRemoteTag retrieves the tag named in the askedTag argument from the given
|
|
||||||
// repository. It queries each of the registries supplied in the registries
|
|
||||||
// argument, and returns data from the first one that answers the query
|
|
||||||
// successfully.
|
|
||||||
func (r *Session) GetRemoteTag(registries []string, repositoryRef reference.Named, askedTag string) (string, error) {
|
|
||||||
repository := repositoryRef.RemoteName()
|
|
||||||
|
|
||||||
if strings.Count(repository, "/") == 0 {
|
|
||||||
// This will be removed once the registry supports auto-resolution on
|
|
||||||
// the "library" namespace
|
|
||||||
repository = "library/" + repository
|
|
||||||
}
|
|
||||||
for _, host := range registries {
|
|
||||||
endpoint := fmt.Sprintf("%srepositories/%s/tags/%s", host, repository, askedTag)
|
|
||||||
res, err := r.client.Get(endpoint)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("Got status code %d from %s", res.StatusCode, endpoint)
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
if res.StatusCode == 404 {
|
|
||||||
return "", ErrRepoNotFound
|
|
||||||
}
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var tagID string
|
|
||||||
if err := json.NewDecoder(res.Body).Decode(&tagID); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return tagID, nil
|
|
||||||
}
|
|
||||||
return "", fmt.Errorf("Could not reach any registry endpoint")
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRemoteTags retrieves all tags from the given repository. It queries each
|
|
||||||
// of the registries supplied in the registries argument, and returns data from
|
|
||||||
// the first one that answers the query successfully. It returns a map with
|
|
||||||
// tag names as the keys and image IDs as the values.
|
|
||||||
func (r *Session) GetRemoteTags(registries []string, repositoryRef reference.Named) (map[string]string, error) {
|
|
||||||
repository := repositoryRef.RemoteName()
|
|
||||||
|
|
||||||
if strings.Count(repository, "/") == 0 {
|
|
||||||
// This will be removed once the registry supports auto-resolution on
|
|
||||||
// the "library" namespace
|
|
||||||
repository = "library/" + repository
|
|
||||||
}
|
|
||||||
for _, host := range registries {
|
|
||||||
endpoint := fmt.Sprintf("%srepositories/%s/tags", host, repository)
|
|
||||||
res, err := r.client.Get(endpoint)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("Got status code %d from %s", res.StatusCode, endpoint)
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
if res.StatusCode == 404 {
|
|
||||||
return nil, ErrRepoNotFound
|
|
||||||
}
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
result := make(map[string]string)
|
|
||||||
if err := json.NewDecoder(res.Body).Decode(&result); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("Could not reach any registry endpoint")
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildEndpointsList(headers []string, indexEp string) ([]string, error) {
|
|
||||||
var endpoints []string
|
|
||||||
parsedURL, err := url.Parse(indexEp)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var urlScheme = parsedURL.Scheme
|
|
||||||
// The registry's URL scheme has to match the Index'
|
|
||||||
for _, ep := range headers {
|
|
||||||
epList := strings.Split(ep, ",")
|
|
||||||
for _, epListElement := range epList {
|
|
||||||
endpoints = append(
|
|
||||||
endpoints,
|
|
||||||
fmt.Sprintf("%s://%s/v1/", urlScheme, strings.TrimSpace(epListElement)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return endpoints, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRepositoryData returns lists of images and endpoints for the repository
|
|
||||||
func (r *Session) GetRepositoryData(name reference.Named) (*RepositoryData, error) {
|
|
||||||
repositoryTarget := fmt.Sprintf("%srepositories/%s/images", r.indexEndpoint.String(), name.RemoteName())
|
|
||||||
|
|
||||||
logrus.Debugf("[registry] Calling GET %s", repositoryTarget)
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", repositoryTarget, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// this will set basic auth in r.client.Transport and send cached X-Docker-Token headers for all subsequent requests
|
|
||||||
req.Header.Set("X-Docker-Token", "true")
|
|
||||||
res, err := r.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
// check if the error is because of i/o timeout
|
|
||||||
// and return a non-obtuse error message for users
|
|
||||||
// "Get https://index.docker.io/v1/repositories/library/busybox/images: i/o timeout"
|
|
||||||
// was a top search on the docker user forum
|
|
||||||
if isTimeout(err) {
|
|
||||||
return nil, fmt.Errorf("Network timed out while trying to connect to %s. You may want to check your internet connection or if you are behind a proxy.", repositoryTarget)
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("Error while pulling image: %v", err)
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
if res.StatusCode == 401 {
|
|
||||||
return nil, errcode.ErrorCodeUnauthorized.WithArgs()
|
|
||||||
}
|
|
||||||
// TODO: Right now we're ignoring checksums in the response body.
|
|
||||||
// In the future, we need to use them to check image validity.
|
|
||||||
if res.StatusCode == 404 {
|
|
||||||
return nil, httputils.NewHTTPRequestError(fmt.Sprintf("HTTP code: %d", res.StatusCode), res)
|
|
||||||
} else if res.StatusCode != 200 {
|
|
||||||
errBody, err := ioutil.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Debugf("Error reading response body: %s", err)
|
|
||||||
}
|
|
||||||
return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Error: Status %d trying to pull repository %s: %q", res.StatusCode, name.RemoteName(), errBody), res)
|
|
||||||
}
|
|
||||||
|
|
||||||
var endpoints []string
|
|
||||||
if res.Header.Get("X-Docker-Endpoints") != "" {
|
|
||||||
endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], r.indexEndpoint.String())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Assume the endpoint is on the same host
|
|
||||||
endpoints = append(endpoints, fmt.Sprintf("%s://%s/v1/", r.indexEndpoint.URL.Scheme, req.URL.Host))
|
|
||||||
}
|
|
||||||
|
|
||||||
remoteChecksums := []*ImgData{}
|
|
||||||
if err := json.NewDecoder(res.Body).Decode(&remoteChecksums); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Forge a better object from the retrieved data
|
|
||||||
imgsData := make(map[string]*ImgData, len(remoteChecksums))
|
|
||||||
for _, elem := range remoteChecksums {
|
|
||||||
imgsData[elem.ID] = elem
|
|
||||||
}
|
|
||||||
|
|
||||||
return &RepositoryData{
|
|
||||||
ImgList: imgsData,
|
|
||||||
Endpoints: endpoints,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PushImageChecksumRegistry uploads checksums for an image
|
|
||||||
func (r *Session) PushImageChecksumRegistry(imgData *ImgData, registry string) error {
|
|
||||||
u := registry + "images/" + imgData.ID + "/checksum"
|
|
||||||
|
|
||||||
logrus.Debugf("[registry] Calling PUT %s", u)
|
|
||||||
|
|
||||||
req, err := http.NewRequest("PUT", u, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req.Header.Set("X-Docker-Checksum", imgData.Checksum)
|
|
||||||
req.Header.Set("X-Docker-Checksum-Payload", imgData.ChecksumPayload)
|
|
||||||
|
|
||||||
res, err := r.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to upload metadata: %v", err)
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
if len(res.Cookies()) > 0 {
|
|
||||||
r.client.Jar.SetCookies(req.URL, res.Cookies())
|
|
||||||
}
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
errBody, err := ioutil.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("HTTP code %d while uploading metadata and error when trying to parse response body: %s", res.StatusCode, err)
|
|
||||||
}
|
|
||||||
var jsonBody map[string]string
|
|
||||||
if err := json.Unmarshal(errBody, &jsonBody); err != nil {
|
|
||||||
errBody = []byte(err.Error())
|
|
||||||
} else if jsonBody["error"] == "Image already exists" {
|
|
||||||
return ErrAlreadyExists
|
|
||||||
}
|
|
||||||
return fmt.Errorf("HTTP code %d while uploading metadata: %q", res.StatusCode, errBody)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PushImageJSONRegistry pushes JSON metadata for a local image to the registry
|
|
||||||
func (r *Session) PushImageJSONRegistry(imgData *ImgData, jsonRaw []byte, registry string) error {
|
|
||||||
|
|
||||||
u := registry + "images/" + imgData.ID + "/json"
|
|
||||||
|
|
||||||
logrus.Debugf("[registry] Calling PUT %s", u)
|
|
||||||
|
|
||||||
req, err := http.NewRequest("PUT", u, bytes.NewReader(jsonRaw))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req.Header.Add("Content-type", "application/json")
|
|
||||||
|
|
||||||
res, err := r.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to upload metadata: %s", err)
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
if res.StatusCode == 401 && strings.HasPrefix(registry, "http://") {
|
|
||||||
return httputils.NewHTTPRequestError("HTTP code 401, Docker will not send auth headers over HTTP.", res)
|
|
||||||
}
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
errBody, err := ioutil.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return httputils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d while uploading metadata and error when trying to parse response body: %s", res.StatusCode, err), res)
|
|
||||||
}
|
|
||||||
var jsonBody map[string]string
|
|
||||||
if err := json.Unmarshal(errBody, &jsonBody); err != nil {
|
|
||||||
errBody = []byte(err.Error())
|
|
||||||
} else if jsonBody["error"] == "Image already exists" {
|
|
||||||
return ErrAlreadyExists
|
|
||||||
}
|
|
||||||
return httputils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d while uploading metadata: %q", res.StatusCode, errBody), res)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PushImageLayerRegistry sends the checksum of an image layer to the registry
|
|
||||||
func (r *Session) PushImageLayerRegistry(imgID string, layer io.Reader, registry string, jsonRaw []byte) (checksum string, checksumPayload string, err error) {
|
|
||||||
u := registry + "images/" + imgID + "/layer"
|
|
||||||
|
|
||||||
logrus.Debugf("[registry] Calling PUT %s", u)
|
|
||||||
|
|
||||||
tarsumLayer, err := tarsum.NewTarSum(layer, false, tarsum.Version0)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
h := sha256.New()
|
|
||||||
h.Write(jsonRaw)
|
|
||||||
h.Write([]byte{'\n'})
|
|
||||||
checksumLayer := io.TeeReader(tarsumLayer, h)
|
|
||||||
|
|
||||||
req, err := http.NewRequest("PUT", u, checksumLayer)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
req.Header.Add("Content-Type", "application/octet-stream")
|
|
||||||
req.ContentLength = -1
|
|
||||||
req.TransferEncoding = []string{"chunked"}
|
|
||||||
res, err := r.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", fmt.Errorf("Failed to upload layer: %v", err)
|
|
||||||
}
|
|
||||||
if rc, ok := layer.(io.Closer); ok {
|
|
||||||
if err := rc.Close(); err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
errBody, err := ioutil.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", httputils.NewHTTPRequestError(fmt.Sprintf("HTTP code %d while uploading metadata and error when trying to parse response body: %s", res.StatusCode, err), res)
|
|
||||||
}
|
|
||||||
return "", "", httputils.NewHTTPRequestError(fmt.Sprintf("Received HTTP code %d while uploading layer: %q", res.StatusCode, errBody), res)
|
|
||||||
}
|
|
||||||
|
|
||||||
checksumPayload = "sha256:" + hex.EncodeToString(h.Sum(nil))
|
|
||||||
return tarsumLayer.Sum(jsonRaw), checksumPayload, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PushRegistryTag pushes a tag on the registry.
|
|
||||||
// Remote has the format '<user>/<repo>
|
|
||||||
func (r *Session) PushRegistryTag(remote reference.Named, revision, tag, registry string) error {
|
|
||||||
// "jsonify" the string
|
|
||||||
revision = "\"" + revision + "\""
|
|
||||||
path := fmt.Sprintf("repositories/%s/tags/%s", remote.RemoteName(), tag)
|
|
||||||
|
|
||||||
req, err := http.NewRequest("PUT", registry+path, strings.NewReader(revision))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req.Header.Add("Content-type", "application/json")
|
|
||||||
req.ContentLength = int64(len(revision))
|
|
||||||
res, err := r.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
res.Body.Close()
|
|
||||||
if res.StatusCode != 200 && res.StatusCode != 201 {
|
|
||||||
return httputils.NewHTTPRequestError(fmt.Sprintf("Internal server error: %d trying to push tag %s on %s", res.StatusCode, tag, remote.RemoteName()), res)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PushImageJSONIndex uploads an image list to the repository
|
|
||||||
func (r *Session) PushImageJSONIndex(remote reference.Named, imgList []*ImgData, validate bool, regs []string) (*RepositoryData, error) {
|
|
||||||
cleanImgList := []*ImgData{}
|
|
||||||
if validate {
|
|
||||||
for _, elem := range imgList {
|
|
||||||
if elem.Checksum != "" {
|
|
||||||
cleanImgList = append(cleanImgList, elem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cleanImgList = imgList
|
|
||||||
}
|
|
||||||
|
|
||||||
imgListJSON, err := json.Marshal(cleanImgList)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var suffix string
|
|
||||||
if validate {
|
|
||||||
suffix = "images"
|
|
||||||
}
|
|
||||||
u := fmt.Sprintf("%srepositories/%s/%s", r.indexEndpoint.String(), remote.RemoteName(), suffix)
|
|
||||||
logrus.Debugf("[registry] PUT %s", u)
|
|
||||||
logrus.Debugf("Image list pushed to index:\n%s", imgListJSON)
|
|
||||||
headers := map[string][]string{
|
|
||||||
"Content-type": {"application/json"},
|
|
||||||
// this will set basic auth in r.client.Transport and send cached X-Docker-Token headers for all subsequent requests
|
|
||||||
"X-Docker-Token": {"true"},
|
|
||||||
}
|
|
||||||
if validate {
|
|
||||||
headers["X-Docker-Endpoints"] = regs
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect if necessary
|
|
||||||
var res *http.Response
|
|
||||||
for {
|
|
||||||
if res, err = r.putImageRequest(u, headers, imgListJSON); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !shouldRedirect(res) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
res.Body.Close()
|
|
||||||
u = res.Header.Get("Location")
|
|
||||||
logrus.Debugf("Redirected to %s", u)
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
if res.StatusCode == 401 {
|
|
||||||
return nil, errcode.ErrorCodeUnauthorized.WithArgs()
|
|
||||||
}
|
|
||||||
|
|
||||||
var tokens, endpoints []string
|
|
||||||
if !validate {
|
|
||||||
if res.StatusCode != 200 && res.StatusCode != 201 {
|
|
||||||
errBody, err := ioutil.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Debugf("Error reading response body: %s", err)
|
|
||||||
}
|
|
||||||
return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Error: Status %d trying to push repository %s: %q", res.StatusCode, remote.RemoteName(), errBody), res)
|
|
||||||
}
|
|
||||||
tokens = res.Header["X-Docker-Token"]
|
|
||||||
logrus.Debugf("Auth token: %v", tokens)
|
|
||||||
|
|
||||||
if res.Header.Get("X-Docker-Endpoints") == "" {
|
|
||||||
return nil, fmt.Errorf("Index response didn't contain any endpoints")
|
|
||||||
}
|
|
||||||
endpoints, err = buildEndpointsList(res.Header["X-Docker-Endpoints"], r.indexEndpoint.String())
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if res.StatusCode != 204 {
|
|
||||||
errBody, err := ioutil.ReadAll(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Debugf("Error reading response body: %s", err)
|
|
||||||
}
|
|
||||||
return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Error: Status %d trying to push checksums %s: %q", res.StatusCode, remote.RemoteName(), errBody), res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &RepositoryData{
|
|
||||||
Endpoints: endpoints,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Session) putImageRequest(u string, headers map[string][]string, body []byte) (*http.Response, error) {
|
|
||||||
req, err := http.NewRequest("PUT", u, bytes.NewReader(body))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.ContentLength = int64(len(body))
|
|
||||||
for k, v := range headers {
|
|
||||||
req.Header[k] = v
|
|
||||||
}
|
|
||||||
response, err := r.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func shouldRedirect(response *http.Response) bool {
|
|
||||||
return response.StatusCode >= 300 && response.StatusCode < 400
|
|
||||||
}
|
|
||||||
|
|
||||||
// SearchRepositories performs a search against the remote repository
|
|
||||||
func (r *Session) SearchRepositories(term string, limit int) (*registrytypes.SearchResults, error) {
|
|
||||||
if limit < 1 || limit > 100 {
|
|
||||||
return nil, fmt.Errorf("Limit %d is outside the range of [1, 100]", limit)
|
|
||||||
}
|
|
||||||
logrus.Debugf("Index server: %s", r.indexEndpoint)
|
|
||||||
u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term) + "&n=" + url.QueryEscape(fmt.Sprintf("%d", limit))
|
|
||||||
|
|
||||||
req, err := http.NewRequest("GET", u, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Error while getting from the server: %v", err)
|
|
||||||
}
|
|
||||||
// Have the AuthTransport send authentication, when logged in.
|
|
||||||
req.Header.Set("X-Docker-Token", "true")
|
|
||||||
res, err := r.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
if res.StatusCode != 200 {
|
|
||||||
return nil, httputils.NewHTTPRequestError(fmt.Sprintf("Unexpected status code %d", res.StatusCode), res)
|
|
||||||
}
|
|
||||||
result := new(registrytypes.SearchResults)
|
|
||||||
return result, json.NewDecoder(res.Body).Decode(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAuthConfig returns the authentication settings for a session
|
|
||||||
// TODO(tiborvass): remove this once registry client v2 is vendored
|
|
||||||
func (r *Session) GetAuthConfig(withPasswd bool) *types.AuthConfig {
|
|
||||||
password := ""
|
|
||||||
if withPasswd {
|
|
||||||
password = r.authConfig.Password
|
|
||||||
}
|
|
||||||
return &types.AuthConfig{
|
|
||||||
Username: r.authConfig.Username,
|
|
||||||
Password: password,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func isTimeout(err error) bool {
|
|
||||||
type timeout interface {
|
|
||||||
Timeout() bool
|
|
||||||
}
|
|
||||||
e := err
|
|
||||||
switch urlErr := err.(type) {
|
|
||||||
case *url.Error:
|
|
||||||
e = urlErr.Err
|
|
||||||
}
|
|
||||||
t, ok := e.(timeout)
|
|
||||||
return ok && t.Timeout()
|
|
||||||
}
|
|
|
@ -1,12 +1,12 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Specification for the Registry API.
|
||||||
title = "HTTP API V2"
|
keywords:
|
||||||
description = "Specification for the Registry API."
|
- registry, on-prem, images, tags, repository, distribution, api, advanced
|
||||||
keywords = ["registry, on-prem, images, tags, repository, distribution, api, advanced"]
|
menu:
|
||||||
[menu.main]
|
main:
|
||||||
parent="smn_registry_ref"
|
parent: smn_registry_ref
|
||||||
+++
|
title: HTTP API V2
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
# Docker Registry HTTP API V2
|
# Docker Registry HTTP API V2
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Specification for the Registry API.
|
||||||
title = "HTTP API V2"
|
keywords:
|
||||||
description = "Specification for the Registry API."
|
- registry, on-prem, images, tags, repository, distribution, api, advanced
|
||||||
keywords = ["registry, on-prem, images, tags, repository, distribution, api, advanced"]
|
menu:
|
||||||
[menu.main]
|
main:
|
||||||
parent="smn_registry_ref"
|
parent: smn_registry_ref
|
||||||
+++
|
title: HTTP API V2
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
# Docker Registry HTTP API V2
|
# Docker Registry HTTP API V2
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Docker Registry v2 authentication schema
|
||||||
title = "Docker Registry Token Authentication"
|
keywords:
|
||||||
description = "Docker Registry v2 authentication schema"
|
- registry, on-prem, images, tags, repository, distribution, authentication, advanced
|
||||||
keywords = ["registry, on-prem, images, tags, repository, distribution, authentication, advanced"]
|
menu:
|
||||||
[menu.main]
|
main:
|
||||||
parent="smn_registry_ref"
|
parent: smn_registry_ref
|
||||||
weight=100
|
weight: 100
|
||||||
+++
|
title: Docker Registry Token Authentication
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
# Docker Registry v2 authentication
|
# Docker Registry v2 authentication
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Describe the reference implementation of the Docker Registry v2 authentication
|
||||||
title = "Token Authentication Implementation"
|
schema
|
||||||
description = "Describe the reference implementation of the Docker Registry v2 authentication schema"
|
keywords:
|
||||||
keywords = ["registry, on-prem, images, tags, repository, distribution, JWT authentication, advanced"]
|
- registry, on-prem, images, tags, repository, distribution, JWT authentication, advanced
|
||||||
[menu.main]
|
menu:
|
||||||
parent="smn_registry_ref"
|
main:
|
||||||
weight=101
|
parent: smn_registry_ref
|
||||||
+++
|
weight: 101
|
||||||
<![end-metadata]-->
|
title: Token Authentication Implementation
|
||||||
|
---
|
||||||
|
|
||||||
# Docker Registry v2 Bearer token specification
|
# Docker Registry v2 Bearer token specification
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Specifies the Docker Registry v2 authentication
|
||||||
title = "Oauth2 Token Authentication"
|
keywords:
|
||||||
description = "Specifies the Docker Registry v2 authentication"
|
- registry, on-prem, images, tags, repository, distribution, oauth2, advanced
|
||||||
keywords = ["registry, on-prem, images, tags, repository, distribution, oauth2, advanced"]
|
menu:
|
||||||
[menu.main]
|
main:
|
||||||
parent="smn_registry_ref"
|
parent: smn_registry_ref
|
||||||
weight=102
|
weight: 102
|
||||||
+++
|
title: Oauth2 Token Authentication
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
# Docker Registry v2 authentication using OAuth2
|
# Docker Registry v2 authentication using OAuth2
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Describes the scope and access fields used for registry authorization
|
||||||
title = "Token Scope Documentation"
|
tokens
|
||||||
description = "Describes the scope and access fields used for registry authorization tokens"
|
keywords:
|
||||||
keywords = ["registry, on-prem, images, tags, repository, distribution, advanced, access, scope"]
|
- registry, on-prem, images, tags, repository, distribution, advanced, access, scope
|
||||||
[menu.main]
|
menu:
|
||||||
parent="smn_registry_ref"
|
main:
|
||||||
weight=103
|
parent: smn_registry_ref
|
||||||
+++
|
weight: 103
|
||||||
<![end-metadata]-->
|
title: Token Scope Documentation
|
||||||
|
---
|
||||||
|
|
||||||
# Docker Registry Token Scope and Access
|
# Docker Registry Token Scope and Access
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Specifies the Docker Registry v2 authentication
|
||||||
title = "Token Authentication Specification"
|
keywords:
|
||||||
description = "Specifies the Docker Registry v2 authentication"
|
- registry, on-prem, images, tags, repository, distribution, Bearer authentication,
|
||||||
keywords = ["registry, on-prem, images, tags, repository, distribution, Bearer authentication, advanced"]
|
advanced
|
||||||
[menu.main]
|
menu:
|
||||||
parent="smn_registry_ref"
|
main:
|
||||||
weight=104
|
parent: smn_registry_ref
|
||||||
+++
|
weight: 104
|
||||||
<![end-metadata]-->
|
title: Token Authentication Specification
|
||||||
|
---
|
||||||
|
|
||||||
# Docker Registry v2 authentication via central service
|
# Docker Registry v2 authentication via central service
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
draft: true
|
||||||
draft = true
|
---
|
||||||
+++
|
|
||||||
<![end-metadata]-->
|
|
||||||
|
|
||||||
# Distribution API Implementations
|
# Distribution API Implementations
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Explains registry JSON objects
|
||||||
title = "Reference Overview"
|
keywords:
|
||||||
description = "Explains registry JSON objects"
|
- registry, service, images, repository, json
|
||||||
keywords = ["registry, service, images, repository, json"]
|
menu:
|
||||||
[menu.main]
|
main:
|
||||||
parent="smn_registry_ref"
|
parent: smn_registry_ref
|
||||||
weight=-1
|
weight: -1
|
||||||
+++
|
title: Reference Overview
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
# Docker Registry Reference
|
# Docker Registry Reference
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Explains registry JSON objects
|
||||||
draft=true
|
draft: true
|
||||||
title = "Docker Distribution JSON Canonicalization"
|
keywords:
|
||||||
description = "Explains registry JSON objects"
|
- registry, service, images, repository, json
|
||||||
keywords = ["registry, service, images, repository, json"]
|
menu:
|
||||||
[menu.main]
|
main:
|
||||||
parent="smn_registry_ref"
|
parent: smn_registry_ref
|
||||||
+++
|
title: Docker Distribution JSON Canonicalization
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Docker Distribution JSON Canonicalization
|
# Docker Distribution JSON Canonicalization
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: image manifest for the Registry.
|
||||||
title = "Image Manifest V 2, Schema 1 "
|
keywords:
|
||||||
description = "image manifest for the Registry."
|
- registry, on-prem, images, tags, repository, distribution, api, advanced, manifest
|
||||||
keywords = ["registry, on-prem, images, tags, repository, distribution, api, advanced, manifest"]
|
menu:
|
||||||
[menu.main]
|
main:
|
||||||
parent="smn_registry_ref"
|
parent: smn_registry_ref
|
||||||
+++
|
title: 'Image Manifest V 2, Schema 1 '
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
# Image Manifest Version 2, Schema 1
|
# Image Manifest Version 2, Schema 1
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: image manifest for the Registry.
|
||||||
title = "Image Manifest V 2, Schema 2 "
|
keywords:
|
||||||
description = "image manifest for the Registry."
|
- registry, on-prem, images, tags, repository, distribution, api, advanced, manifest
|
||||||
keywords = ["registry, on-prem, images, tags, repository, distribution, api, advanced, manifest"]
|
menu:
|
||||||
[menu.main]
|
main:
|
||||||
parent="smn_registry_ref"
|
parent: smn_registry_ref
|
||||||
+++
|
title: 'Image Manifest V 2, Schema 2 '
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
# Image Manifest Version 2, Schema 2
|
# Image Manifest Version 2, Schema 2
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Explains registry JSON objects
|
||||||
title = "Reference"
|
keywords:
|
||||||
description = "Explains registry JSON objects"
|
- registry, service, images, repository, json
|
||||||
keywords = ["registry, service, images, repository, json"]
|
menu:
|
||||||
type = "menu"
|
main:
|
||||||
[menu.main]
|
identifier: smn_registry_ref
|
||||||
identifier="smn_registry_ref"
|
parent: smn_registry
|
||||||
parent="smn_registry"
|
weight: 7
|
||||||
weight=7
|
title: Reference
|
||||||
+++
|
type: menu
|
||||||
|
---
|
||||||
|
|
||||||
<![end-metadata]-->
|
<![end-metadata]-->
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Explains how to use the Azure storage drivers
|
||||||
title = "Microsoft Azure storage driver"
|
keywords:
|
||||||
description = "Explains how to use the Azure storage drivers"
|
- registry, service, driver, images, storage, azure
|
||||||
keywords = ["registry, service, driver, images, storage, azure"]
|
menu:
|
||||||
[menu.main]
|
main:
|
||||||
parent = "smn_storagedrivers"
|
parent: smn_storagedrivers
|
||||||
+++
|
title: Microsoft Azure storage driver
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
|
|
||||||
# Microsoft Azure storage driver
|
# Microsoft Azure storage driver
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Explains how to use the filesystem storage drivers
|
||||||
title = "Filesystem storage driver"
|
keywords:
|
||||||
description = "Explains how to use the filesystem storage drivers"
|
- registry, service, driver, images, storage, filesystem
|
||||||
keywords = ["registry, service, driver, images, storage, filesystem"]
|
menu:
|
||||||
[menu.main]
|
main:
|
||||||
parent="smn_storagedrivers"
|
parent: smn_storagedrivers
|
||||||
+++
|
title: Filesystem storage driver
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
|
|
||||||
# Filesystem storage driver
|
# Filesystem storage driver
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Explains how to use the Google Cloud Storage drivers
|
||||||
title = "GCS storage driver"
|
keywords:
|
||||||
description = "Explains how to use the Google Cloud Storage drivers"
|
- registry, service, driver, images, storage, gcs, google, cloud
|
||||||
keywords = ["registry, service, driver, images, storage, gcs, google, cloud"]
|
menu:
|
||||||
[menu.main]
|
main:
|
||||||
parent="smn_storagedrivers"
|
parent: smn_storagedrivers
|
||||||
+++
|
title: GCS storage driver
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
|
|
||||||
# Google Cloud Storage driver
|
# Google Cloud Storage driver
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
aliases:
|
||||||
title = "Storage Driver overview"
|
- /registry/storagedrivers/
|
||||||
description = "Explains how to use storage drivers"
|
description: Explains how to use storage drivers
|
||||||
keywords = ["registry, on-prem, images, tags, repository, distribution, storage drivers, advanced"]
|
keywords:
|
||||||
aliases = ["/registry/storagedrivers/"]
|
- registry, on-prem, images, tags, repository, distribution, storage drivers, advanced
|
||||||
[menu.main]
|
menu:
|
||||||
parent="smn_storagedrivers"
|
main:
|
||||||
identifier="storage_index"
|
identifier: storage_index
|
||||||
weight=-1
|
parent: smn_storagedrivers
|
||||||
+++
|
weight: -1
|
||||||
<![end-metadata]-->
|
title: Storage Driver overview
|
||||||
|
---
|
||||||
|
|
||||||
# Docker Registry Storage Driver
|
# Docker Registry Storage Driver
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Explains how to use the in-memory storage drivers
|
||||||
title = "In-memory storage driver"
|
keywords:
|
||||||
description = "Explains how to use the in-memory storage drivers"
|
- registry, service, driver, images, storage, in-memory
|
||||||
keywords = ["registry, service, driver, images, storage, in-memory"]
|
menu:
|
||||||
[menu.main]
|
main:
|
||||||
parent="smn_storagedrivers"
|
parent: smn_storagedrivers
|
||||||
+++
|
title: In-memory storage driver
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
|
|
||||||
# In-memory storage driver (Testing Only)
|
# In-memory storage driver (Testing Only)
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Storage Drivers
|
||||||
title = "Storage Drivers"
|
keywords:
|
||||||
description = "Storage Drivers"
|
- registry, on-prem, images, tags, repository, distribution
|
||||||
keywords = ["registry, on-prem, images, tags, repository, distribution"]
|
menu:
|
||||||
type = "menu"
|
main:
|
||||||
[menu.main]
|
identifier: smn_storagedrivers
|
||||||
identifier="smn_storagedrivers"
|
parent: smn_registry
|
||||||
parent="smn_registry"
|
weight: 7
|
||||||
weight=7
|
title: Storage Drivers
|
||||||
+++
|
type: menu
|
||||||
|
---
|
||||||
|
|
||||||
<![end-metadata]-->
|
<![end-metadata]-->
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Explains how to use the Aliyun OSS storage driver
|
||||||
title = "Aliyun OSS storage driver"
|
keywords:
|
||||||
description = "Explains how to use the Aliyun OSS storage driver"
|
- registry, service, driver, images, storage, OSS, aliyun
|
||||||
keywords = ["registry, service, driver, images, storage, OSS, aliyun"]
|
menu:
|
||||||
[menu.main]
|
main:
|
||||||
parent="smn_storagedrivers"
|
parent: smn_storagedrivers
|
||||||
+++
|
title: Aliyun OSS storage driver
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
# Aliyun OSS storage driver
|
# Aliyun OSS storage driver
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Explains how to use the S3 storage drivers
|
||||||
title = "S3 storage driver"
|
keywords:
|
||||||
description = "Explains how to use the S3 storage drivers"
|
- registry, service, driver, images, storage, S3
|
||||||
keywords = ["registry, service, driver, images, storage, S3"]
|
menu:
|
||||||
[menu.main]
|
main:
|
||||||
parent="smn_storagedrivers"
|
parent: smn_storagedrivers
|
||||||
+++
|
title: S3 storage driver
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
|
|
||||||
# S3 storage driver
|
# S3 storage driver
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
<!--[metadata]>
|
---
|
||||||
+++
|
description: Explains how to use the OpenStack swift storage driver
|
||||||
title = "Swift storage driver"
|
keywords:
|
||||||
description = "Explains how to use the OpenStack swift storage driver"
|
- registry, service, driver, images, storage, swift
|
||||||
keywords = ["registry, service, driver, images, storage, swift"]
|
menu:
|
||||||
[menu.main]
|
main:
|
||||||
parent="smn_storagedrivers"
|
parent: smn_storagedrivers
|
||||||
+++
|
title: Swift storage driver
|
||||||
<![end-metadata]-->
|
---
|
||||||
|
|
||||||
|
|
||||||
# OpenStack Swift storage driver
|
# OpenStack Swift storage driver
|
||||||
|
|
||||||
|
|
|
@ -1,70 +0,0 @@
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/docker/docker/reference"
|
|
||||||
registrytypes "github.com/docker/engine-api/types/registry"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RepositoryData tracks the image list, list of endpoints, and list of tokens
|
|
||||||
// for a repository
|
|
||||||
type RepositoryData struct {
|
|
||||||
// ImgList is a list of images in the repository
|
|
||||||
ImgList map[string]*ImgData
|
|
||||||
// Endpoints is a list of endpoints returned in X-Docker-Endpoints
|
|
||||||
Endpoints []string
|
|
||||||
// Tokens is currently unused (remove it?)
|
|
||||||
Tokens []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ImgData is used to transfer image checksums to and from the registry
|
|
||||||
type ImgData struct {
|
|
||||||
// ID is an opaque string that identifies the image
|
|
||||||
ID string `json:"id"`
|
|
||||||
Checksum string `json:"checksum,omitempty"`
|
|
||||||
ChecksumPayload string `json:"-"`
|
|
||||||
Tag string `json:",omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// PingResult contains the information returned when pinging a registry. It
|
|
||||||
// indicates the registry's version and whether the registry claims to be a
|
|
||||||
// standalone registry.
|
|
||||||
type PingResult struct {
|
|
||||||
// Version is the registry version supplied by the registry in an HTTP
|
|
||||||
// header
|
|
||||||
Version string `json:"version"`
|
|
||||||
// Standalone is set to true if the registry indicates it is a
|
|
||||||
// standalone registry in the X-Docker-Registry-Standalone
|
|
||||||
// header
|
|
||||||
Standalone bool `json:"standalone"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// APIVersion is an integral representation of an API version (presently
|
|
||||||
// either 1 or 2)
|
|
||||||
type APIVersion int
|
|
||||||
|
|
||||||
func (av APIVersion) String() string {
|
|
||||||
return apiVersions[av]
|
|
||||||
}
|
|
||||||
|
|
||||||
// API Version identifiers.
|
|
||||||
const (
|
|
||||||
_ = iota
|
|
||||||
APIVersion1 APIVersion = iota
|
|
||||||
APIVersion2
|
|
||||||
)
|
|
||||||
|
|
||||||
var apiVersions = map[APIVersion]string{
|
|
||||||
APIVersion1: "v1",
|
|
||||||
APIVersion2: "v2",
|
|
||||||
}
|
|
||||||
|
|
||||||
// RepositoryInfo describes a repository
|
|
||||||
type RepositoryInfo struct {
|
|
||||||
reference.Named
|
|
||||||
// Index points to registry information
|
|
||||||
Index *registrytypes.IndexInfo
|
|
||||||
// Official indicates whether the repository is considered official.
|
|
||||||
// If the registry is official, and the normalized name does not
|
|
||||||
// contain a '/' (e.g. "foo"), then it is considered an official repo.
|
|
||||||
Official bool
|
|
||||||
}
|
|
Loading…
Reference in a new issue