Deprecating ResolveRepositoryName
Passing RepositoryInfo to ResolveAuthConfig, pullRepository, and pushRepository Moving --registry-mirror configuration to registry config Created resolve_repository job Repo names with 'index.docker.io' or 'docker.io' are now synonymous with omitting an index name. Adding test for RepositoryInfo Adding tests for opts.StringSetOpts and registry.ValidateMirror Fixing search term use of repoInfo Adding integration tests for registry mirror configuration Normalizing LookupImage image name to match LocalName parsing rules Normalizing repository LocalName to avoid multiple references to an official image Removing errorOut use in tests Removing TODO comment gofmt changes golint comments cleanup. renaming RegistryOptions => registry.Options, and RegistryServiceConfig => registry.ServiceConfig Splitting out builtins.Registry and registry.NewService calls Stray whitespace cleanup Moving integration tests for Mirrors and InsecureRegistries into TestNewIndexInfo unit test Factoring out ValidateRepositoryName from NewRepositoryInfo Removing unused IndexServerURL Allowing json marshaling of ServiceConfig. Exposing ServiceConfig in /info Switching to CamelCase for json marshaling PR cleanup; removing 'Is' prefix from boolean members. Removing unneeded json tags. Removing non-cleanup related fix for 'localhost:[port]' in splitReposName Merge fixes for gh9735 Fixing integration test Reapplying #9754 Adding comment on config.IndexConfigs use from isSecureIndex Remove unused error return value from isSecureIndex Signed-off-by: Don Kjer <don.kjer@gmail.com> Adding back comment in isSecureIndex Signed-off-by: Don Kjer <don.kjer@gmail.com>
This commit is contained in:
parent
eb9ddb7b86
commit
64b000c3ea
11 changed files with 1179 additions and 156 deletions
33
docs/auth.go
33
docs/auth.go
|
@ -7,7 +7,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -22,23 +21,15 @@ const (
|
||||||
// Only used for user auth + account creation
|
// Only used for user auth + account creation
|
||||||
INDEXSERVER = "https://index.docker.io/v1/"
|
INDEXSERVER = "https://index.docker.io/v1/"
|
||||||
REGISTRYSERVER = "https://registry-1.docker.io/v1/"
|
REGISTRYSERVER = "https://registry-1.docker.io/v1/"
|
||||||
|
INDEXNAME = "docker.io"
|
||||||
|
|
||||||
// INDEXSERVER = "https://registry-stage.hub.docker.com/v1/"
|
// INDEXSERVER = "https://registry-stage.hub.docker.com/v1/"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrConfigFileMissing = errors.New("The Auth config file is missing")
|
ErrConfigFileMissing = errors.New("The Auth config file is missing")
|
||||||
IndexServerURL *url.URL
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
|
||||||
url, err := url.Parse(INDEXSERVER)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
IndexServerURL = url
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuthConfig struct {
|
type AuthConfig struct {
|
||||||
Username string `json:"username,omitempty"`
|
Username string `json:"username,omitempty"`
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
|
@ -56,6 +47,10 @@ func IndexServerAddress() string {
|
||||||
return INDEXSERVER
|
return INDEXSERVER
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IndexServerName() string {
|
||||||
|
return INDEXNAME
|
||||||
|
}
|
||||||
|
|
||||||
// create a base64 encoded auth string to store in config
|
// create a base64 encoded auth string to store in config
|
||||||
func encodeAuth(authConfig *AuthConfig) string {
|
func encodeAuth(authConfig *AuthConfig) string {
|
||||||
authStr := authConfig.Username + ":" + authConfig.Password
|
authStr := authConfig.Username + ":" + authConfig.Password
|
||||||
|
@ -118,6 +113,7 @@ func LoadConfig(rootPath string) (*ConfigFile, error) {
|
||||||
}
|
}
|
||||||
authConfig.Email = origEmail[1]
|
authConfig.Email = origEmail[1]
|
||||||
authConfig.ServerAddress = IndexServerAddress()
|
authConfig.ServerAddress = IndexServerAddress()
|
||||||
|
// *TODO: Switch to using IndexServerName() instead?
|
||||||
configFile.Configs[IndexServerAddress()] = authConfig
|
configFile.Configs[IndexServerAddress()] = authConfig
|
||||||
} else {
|
} else {
|
||||||
for k, authConfig := range configFile.Configs {
|
for k, authConfig := range configFile.Configs {
|
||||||
|
@ -181,7 +177,7 @@ func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, e
|
||||||
)
|
)
|
||||||
|
|
||||||
if serverAddress == "" {
|
if serverAddress == "" {
|
||||||
serverAddress = IndexServerAddress()
|
return "", fmt.Errorf("Server Error: Server Address not set.")
|
||||||
}
|
}
|
||||||
|
|
||||||
loginAgainstOfficialIndex := serverAddress == IndexServerAddress()
|
loginAgainstOfficialIndex := serverAddress == IndexServerAddress()
|
||||||
|
@ -213,6 +209,7 @@ func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, e
|
||||||
status = "Account created. Please use the confirmation link we sent" +
|
status = "Account created. Please use the confirmation link we sent" +
|
||||||
" to your e-mail to activate it."
|
" to your e-mail to activate it."
|
||||||
} else {
|
} else {
|
||||||
|
// *TODO: Use registry configuration to determine what this says, if anything?
|
||||||
status = "Account created. Please see the documentation of the registry " + serverAddress + " for instructions how to activate it."
|
status = "Account created. Please see the documentation of the registry " + serverAddress + " for instructions how to activate it."
|
||||||
}
|
}
|
||||||
} else if reqStatusCode == 400 {
|
} else if reqStatusCode == 400 {
|
||||||
|
@ -236,6 +233,7 @@ func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, e
|
||||||
if loginAgainstOfficialIndex {
|
if loginAgainstOfficialIndex {
|
||||||
return "", fmt.Errorf("Login: Account is not Active. Please check your e-mail for a confirmation link.")
|
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)
|
return "", fmt.Errorf("Login: Account is not Active. Please see the documentation of the registry %s for instructions how to activate it.", serverAddress)
|
||||||
}
|
}
|
||||||
return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, resp.StatusCode, resp.Header)
|
return "", fmt.Errorf("Login: %s (Code: %d; Headers: %s)", body, resp.StatusCode, resp.Header)
|
||||||
|
@ -271,14 +269,10 @@ func Login(authConfig *AuthConfig, factory *utils.HTTPRequestFactory) (string, e
|
||||||
}
|
}
|
||||||
|
|
||||||
// this method matches a auth configuration to a server address or a url
|
// this method matches a auth configuration to a server address or a url
|
||||||
func (config *ConfigFile) ResolveAuthConfig(hostname string) AuthConfig {
|
func (config *ConfigFile) ResolveAuthConfig(index *IndexInfo) AuthConfig {
|
||||||
if hostname == IndexServerAddress() || len(hostname) == 0 {
|
configKey := index.GetAuthConfigKey()
|
||||||
// default to the index server
|
|
||||||
return config.Configs[IndexServerAddress()]
|
|
||||||
}
|
|
||||||
|
|
||||||
// First try the happy case
|
// First try the happy case
|
||||||
if c, found := config.Configs[hostname]; found {
|
if c, found := config.Configs[configKey]; found || index.Official {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -297,9 +291,8 @@ func (config *ConfigFile) ResolveAuthConfig(hostname string) AuthConfig {
|
||||||
|
|
||||||
// Maybe they have a legacy config file, we will iterate the keys converting
|
// Maybe they have a legacy config file, we will iterate the keys converting
|
||||||
// them to the new format and testing
|
// them to the new format and testing
|
||||||
normalizedHostename := convertToHostname(hostname)
|
|
||||||
for registry, config := range config.Configs {
|
for registry, config := range config.Configs {
|
||||||
if registryHostname := convertToHostname(registry); registryHostname == normalizedHostename {
|
if configKey == convertToHostname(registry) {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,12 +81,20 @@ func TestResolveAuthConfigIndexServer(t *testing.T) {
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(configFile.rootPath)
|
defer os.RemoveAll(configFile.rootPath)
|
||||||
|
|
||||||
for _, registry := range []string{"", IndexServerAddress()} {
|
indexConfig := configFile.Configs[IndexServerAddress()]
|
||||||
resolved := configFile.ResolveAuthConfig(registry)
|
|
||||||
if resolved != configFile.Configs[IndexServerAddress()] {
|
officialIndex := &IndexInfo{
|
||||||
t.Fail()
|
Official: true,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
privateIndex := &IndexInfo{
|
||||||
|
Official: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved := configFile.ResolveAuthConfig(officialIndex)
|
||||||
|
assertEqual(t, resolved, indexConfig, "Expected ResolveAuthConfig to return IndexServerAddress()")
|
||||||
|
|
||||||
|
resolved = configFile.ResolveAuthConfig(privateIndex)
|
||||||
|
assertNotEqual(t, resolved, indexConfig, "Expected ResolveAuthConfig to not return IndexServerAddress()")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolveAuthConfigFullURL(t *testing.T) {
|
func TestResolveAuthConfigFullURL(t *testing.T) {
|
||||||
|
@ -106,18 +114,27 @@ func TestResolveAuthConfigFullURL(t *testing.T) {
|
||||||
Password: "bar-pass",
|
Password: "bar-pass",
|
||||||
Email: "bar@example.com",
|
Email: "bar@example.com",
|
||||||
}
|
}
|
||||||
configFile.Configs["https://registry.example.com/v1/"] = registryAuth
|
officialAuth := AuthConfig{
|
||||||
configFile.Configs["http://localhost:8000/v1/"] = localAuth
|
Username: "baz-user",
|
||||||
configFile.Configs["registry.com"] = registryAuth
|
Password: "baz-pass",
|
||||||
|
Email: "baz@example.com",
|
||||||
|
}
|
||||||
|
configFile.Configs[IndexServerAddress()] = officialAuth
|
||||||
|
|
||||||
|
expectedAuths := map[string]AuthConfig{
|
||||||
|
"registry.example.com": registryAuth,
|
||||||
|
"localhost:8000": localAuth,
|
||||||
|
"registry.com": localAuth,
|
||||||
|
}
|
||||||
|
|
||||||
validRegistries := map[string][]string{
|
validRegistries := map[string][]string{
|
||||||
"https://registry.example.com/v1/": {
|
"registry.example.com": {
|
||||||
"https://registry.example.com/v1/",
|
"https://registry.example.com/v1/",
|
||||||
"http://registry.example.com/v1/",
|
"http://registry.example.com/v1/",
|
||||||
"registry.example.com",
|
"registry.example.com",
|
||||||
"registry.example.com/v1/",
|
"registry.example.com/v1/",
|
||||||
},
|
},
|
||||||
"http://localhost:8000/v1/": {
|
"localhost:8000": {
|
||||||
"https://localhost:8000/v1/",
|
"https://localhost:8000/v1/",
|
||||||
"http://localhost:8000/v1/",
|
"http://localhost:8000/v1/",
|
||||||
"localhost:8000",
|
"localhost:8000",
|
||||||
|
@ -132,18 +149,24 @@ func TestResolveAuthConfigFullURL(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for configKey, registries := range validRegistries {
|
for configKey, registries := range validRegistries {
|
||||||
|
configured, ok := expectedAuths[configKey]
|
||||||
|
if !ok || configured.Email == "" {
|
||||||
|
t.Fatal()
|
||||||
|
}
|
||||||
|
index := &IndexInfo{
|
||||||
|
Name: configKey,
|
||||||
|
}
|
||||||
for _, registry := range registries {
|
for _, registry := range registries {
|
||||||
var (
|
configFile.Configs[registry] = configured
|
||||||
configured AuthConfig
|
resolved := configFile.ResolveAuthConfig(index)
|
||||||
ok bool
|
|
||||||
)
|
|
||||||
resolved := configFile.ResolveAuthConfig(registry)
|
|
||||||
if configured, ok = configFile.Configs[configKey]; !ok {
|
|
||||||
t.Fail()
|
|
||||||
}
|
|
||||||
if resolved.Email != configured.Email {
|
if resolved.Email != configured.Email {
|
||||||
t.Errorf("%s -> %q != %q\n", registry, resolved.Email, configured.Email)
|
t.Errorf("%s -> %q != %q\n", registry, resolved.Email, configured.Email)
|
||||||
}
|
}
|
||||||
|
delete(configFile.Configs, registry)
|
||||||
|
resolved = configFile.ResolveAuthConfig(index)
|
||||||
|
if resolved.Email == configured.Email {
|
||||||
|
t.Errorf("%s -> %q == %q\n", registry, resolved.Email, configured.Email)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
126
docs/config.go
Normal file
126
docs/config.go
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
package registry
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/docker/docker/opts"
|
||||||
|
flag "github.com/docker/docker/pkg/mflag"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Options holds command line options.
|
||||||
|
type Options struct {
|
||||||
|
Mirrors opts.ListOpts
|
||||||
|
InsecureRegistries opts.ListOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstallFlags adds command-line options to the top-level flag parser for
|
||||||
|
// the current process.
|
||||||
|
func (options *Options) InstallFlags() {
|
||||||
|
options.Mirrors = opts.NewListOpts(ValidateMirror)
|
||||||
|
flag.Var(&options.Mirrors, []string{"-registry-mirror"}, "Specify a preferred Docker registry mirror")
|
||||||
|
options.InsecureRegistries = opts.NewListOpts(ValidateIndexName)
|
||||||
|
flag.Var(&options.InsecureRegistries, []string{"-insecure-registry"}, "Enable insecure communication with specified registries (no certificate verification for HTTPS and enable HTTP fallback) (e.g., localhost:5000 or 10.20.0.0/16)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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/v1/", uri.Scheme, uri.Host), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateIndexName validates an index name.
|
||||||
|
func ValidateIndexName(val string) (string, error) {
|
||||||
|
// 'index.docker.io' => 'docker.io'
|
||||||
|
if val == "index."+IndexServerName() {
|
||||||
|
val = IndexServerName()
|
||||||
|
}
|
||||||
|
// *TODO: Check if valid hostname[:port]/ip[:port]?
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type netIPNet net.IPNet
|
||||||
|
|
||||||
|
func (ipnet *netIPNet) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal((*net.IPNet)(ipnet).String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ipnet *netIPNet) UnmarshalJSON(b []byte) (err error) {
|
||||||
|
var ipnet_str string
|
||||||
|
if err = json.Unmarshal(b, &ipnet_str); err == nil {
|
||||||
|
var cidr *net.IPNet
|
||||||
|
if _, cidr, err = net.ParseCIDR(ipnet_str); err == nil {
|
||||||
|
*ipnet = netIPNet(*cidr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServiceConfig stores daemon registry services configuration.
|
||||||
|
type ServiceConfig struct {
|
||||||
|
InsecureRegistryCIDRs []*netIPNet `json:"InsecureRegistryCIDRs"`
|
||||||
|
IndexConfigs map[string]*IndexInfo `json:"IndexConfigs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewServiceConfig returns a new instance of ServiceConfig
|
||||||
|
func NewServiceConfig(options *Options) *ServiceConfig {
|
||||||
|
if options == nil {
|
||||||
|
options = &Options{
|
||||||
|
Mirrors: opts.NewListOpts(nil),
|
||||||
|
InsecureRegistries: opts.NewListOpts(nil),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.Set("127.0.0.0/8")
|
||||||
|
|
||||||
|
config := &ServiceConfig{
|
||||||
|
InsecureRegistryCIDRs: make([]*netIPNet, 0),
|
||||||
|
IndexConfigs: make(map[string]*IndexInfo, 0),
|
||||||
|
}
|
||||||
|
// Split --insecure-registry into CIDR and registry-specific settings.
|
||||||
|
for _, r := range options.InsecureRegistries.GetAll() {
|
||||||
|
// Check if CIDR was passed to --insecure-registry
|
||||||
|
_, ipnet, err := net.ParseCIDR(r)
|
||||||
|
if err == nil {
|
||||||
|
// Valid CIDR.
|
||||||
|
config.InsecureRegistryCIDRs = append(config.InsecureRegistryCIDRs, (*netIPNet)(ipnet))
|
||||||
|
} else {
|
||||||
|
// Assume `host:port` if not CIDR.
|
||||||
|
config.IndexConfigs[r] = &IndexInfo{
|
||||||
|
Name: r,
|
||||||
|
Mirrors: make([]string, 0),
|
||||||
|
Secure: false,
|
||||||
|
Official: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure public registry.
|
||||||
|
config.IndexConfigs[IndexServerName()] = &IndexInfo{
|
||||||
|
Name: IndexServerName(),
|
||||||
|
Mirrors: options.Mirrors.GetAll(),
|
||||||
|
Secure: true,
|
||||||
|
Official: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
49
docs/config_test.go
Normal file
49
docs/config_test.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -37,8 +37,9 @@ func scanForAPIVersion(hostname string) (string, APIVersion) {
|
||||||
return hostname, DefaultAPIVersion
|
return hostname, DefaultAPIVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewEndpoint(hostname string, insecureRegistries []string) (*Endpoint, error) {
|
func NewEndpoint(index *IndexInfo) (*Endpoint, error) {
|
||||||
endpoint, err := newEndpoint(hostname, insecureRegistries)
|
// *TODO: Allow per-registry configuration of endpoints.
|
||||||
|
endpoint, err := newEndpoint(index.GetAuthConfigKey(), index.Secure)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -49,7 +50,7 @@ func NewEndpoint(hostname string, insecureRegistries []string) (*Endpoint, error
|
||||||
|
|
||||||
//TODO: triggering highland build can be done there without "failing"
|
//TODO: triggering highland build can be done there without "failing"
|
||||||
|
|
||||||
if endpoint.secure {
|
if index.Secure {
|
||||||
// If registry is secure and HTTPS failed, show user the error and tell them about `--insecure-registry`
|
// 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.
|
// in case that's what they need. DO NOT accept unknown CA certificates, and DO NOT fallback to HTTP.
|
||||||
return nil, fmt.Errorf("Invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %s` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host)
|
return nil, fmt.Errorf("Invalid registry endpoint %s: %v. If this private registry supports only HTTP or HTTPS with an unknown CA certificate, please add `--insecure-registry %s` to the daemon's arguments. In the case of HTTPS, if you have access to the registry's CA certificate, no need for the flag; simply place the CA certificate at /etc/docker/certs.d/%s/ca.crt", endpoint, err, endpoint.URL.Host, endpoint.URL.Host)
|
||||||
|
@ -68,7 +69,7 @@ func NewEndpoint(hostname string, insecureRegistries []string) (*Endpoint, error
|
||||||
|
|
||||||
return endpoint, nil
|
return endpoint, nil
|
||||||
}
|
}
|
||||||
func newEndpoint(hostname string, insecureRegistries []string) (*Endpoint, error) {
|
func newEndpoint(hostname string, secure bool) (*Endpoint, error) {
|
||||||
var (
|
var (
|
||||||
endpoint = Endpoint{}
|
endpoint = Endpoint{}
|
||||||
trimmedHostname string
|
trimmedHostname string
|
||||||
|
@ -82,13 +83,14 @@ func newEndpoint(hostname string, insecureRegistries []string) (*Endpoint, error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
endpoint.secure, err = isSecure(endpoint.URL.Host, insecureRegistries)
|
endpoint.secure = secure
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &endpoint, nil
|
return &endpoint, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (repoInfo *RepositoryInfo) GetEndpoint() (*Endpoint, error) {
|
||||||
|
return NewEndpoint(repoInfo.Index)
|
||||||
|
}
|
||||||
|
|
||||||
type Endpoint struct {
|
type Endpoint struct {
|
||||||
URL *url.URL
|
URL *url.URL
|
||||||
Version APIVersion
|
Version APIVersion
|
||||||
|
@ -156,27 +158,30 @@ func (e Endpoint) Ping() (RegistryInfo, error) {
|
||||||
return info, nil
|
return info, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// isSecure returns false if the provided hostname is part of the list of insecure registries.
|
// 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.
|
// 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.
|
// 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 hostname, the latter is considered
|
// If the subnet contains one of the IPs of the registry specified by indexName, the latter is considered
|
||||||
// insecure.
|
// insecure.
|
||||||
//
|
//
|
||||||
// hostname should be a URL.Host (`host:port` or `host`) where the `host` part can be either a domain name
|
// 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
|
// 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, isSecure will only try to match hostname to any element
|
// in a subnet. If the resolving is not successful, isSecureIndex will only try to match hostname to any element
|
||||||
// of insecureRegistries.
|
// of insecureRegistries.
|
||||||
func isSecure(hostname string, insecureRegistries []string) (bool, error) {
|
func (config *ServiceConfig) isSecureIndex(indexName string) bool {
|
||||||
if hostname == IndexServerURL.Host {
|
// Check for configured index, first. This is needed in case isSecureIndex
|
||||||
return true, nil
|
// 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(hostname)
|
host, _, err := net.SplitHostPort(indexName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// assume hostname is of the form `host` without the port and go on.
|
// assume indexName is of the form `host` without the port and go on.
|
||||||
host = hostname
|
host = indexName
|
||||||
}
|
}
|
||||||
|
|
||||||
addrs, err := lookupIP(host)
|
addrs, err := lookupIP(host)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ip := net.ParseIP(host)
|
ip := net.ParseIP(host)
|
||||||
|
@ -189,29 +194,15 @@ func isSecure(hostname string, insecureRegistries []string) (bool, error) {
|
||||||
// So, len(addrs) == 0 and we're not aborting.
|
// So, len(addrs) == 0 and we're not aborting.
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, r := range insecureRegistries {
|
// Try CIDR notation only if addrs has any elements, i.e. if `host`'s IP could be determined.
|
||||||
if hostname == r {
|
for _, addr := range addrs {
|
||||||
// hostname matches insecure registry
|
for _, ipnet := range config.InsecureRegistryCIDRs {
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try CIDR notation only if addrs has any elements, i.e. if `host`'s IP could be determined.
|
|
||||||
for _, addr := range addrs {
|
|
||||||
|
|
||||||
// now assume a CIDR was passed to --insecure-registry
|
|
||||||
_, ipnet, err := net.ParseCIDR(r)
|
|
||||||
if err != nil {
|
|
||||||
// if we could not parse it as a CIDR, even after removing
|
|
||||||
// assume it's not a CIDR and go on with the next candidate
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the addr falls in the subnet
|
// check if the addr falls in the subnet
|
||||||
if ipnet.Contains(addr) {
|
if (*net.IPNet)(ipnet).Contains(addr) {
|
||||||
return false, nil
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, nil
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ func TestEndpointParse(t *testing.T) {
|
||||||
{"0.0.0.0:5000", "https://0.0.0.0:5000/v1/"},
|
{"0.0.0.0:5000", "https://0.0.0.0:5000/v1/"},
|
||||||
}
|
}
|
||||||
for _, td := range testData {
|
for _, td := range testData {
|
||||||
e, err := newEndpoint(td.str, insecureRegistries)
|
e, err := newEndpoint(td.str, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("%q: %s", td.str, err)
|
t.Errorf("%q: %s", td.str, err)
|
||||||
}
|
}
|
||||||
|
|
165
docs/registry.go
165
docs/registry.go
|
@ -25,6 +25,7 @@ var (
|
||||||
errLoginRequired = errors.New("Authentication is required.")
|
errLoginRequired = errors.New("Authentication is required.")
|
||||||
validNamespaceChars = regexp.MustCompile(`^([a-z0-9-_]*)$`)
|
validNamespaceChars = regexp.MustCompile(`^([a-z0-9-_]*)$`)
|
||||||
validRepo = regexp.MustCompile(`^([a-z0-9-_.]+)$`)
|
validRepo = regexp.MustCompile(`^([a-z0-9-_.]+)$`)
|
||||||
|
emptyServiceConfig = NewServiceConfig(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
type TimeoutType uint32
|
type TimeoutType uint32
|
||||||
|
@ -160,12 +161,12 @@ func doRequest(req *http.Request, jar http.CookieJar, timeout TimeoutType, secur
|
||||||
return res, client, err
|
return res, client, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateRepositoryName(repositoryName string) error {
|
func validateRemoteName(remoteName string) error {
|
||||||
var (
|
var (
|
||||||
namespace string
|
namespace string
|
||||||
name string
|
name string
|
||||||
)
|
)
|
||||||
nameParts := strings.SplitN(repositoryName, "/", 2)
|
nameParts := strings.SplitN(remoteName, "/", 2)
|
||||||
if len(nameParts) < 2 {
|
if len(nameParts) < 2 {
|
||||||
namespace = "library"
|
namespace = "library"
|
||||||
name = nameParts[0]
|
name = nameParts[0]
|
||||||
|
@ -196,29 +197,147 @@ func validateRepositoryName(repositoryName string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolves a repository name to a hostname + name
|
// NewIndexInfo returns IndexInfo configuration from indexName
|
||||||
func ResolveRepositoryName(reposName string) (string, string, error) {
|
func NewIndexInfo(config *ServiceConfig, indexName string) (*IndexInfo, error) {
|
||||||
if strings.Contains(reposName, "://") {
|
var err error
|
||||||
// It cannot contain a scheme!
|
indexName, err = ValidateIndexName(indexName)
|
||||||
return "", "", ErrInvalidRepositoryName
|
if err != nil {
|
||||||
}
|
return nil, err
|
||||||
nameParts := strings.SplitN(reposName, "/", 2)
|
|
||||||
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)
|
|
||||||
err := validateRepositoryName(reposName)
|
|
||||||
return IndexServerAddress(), reposName, err
|
|
||||||
}
|
|
||||||
hostname := nameParts[0]
|
|
||||||
reposName = nameParts[1]
|
|
||||||
if strings.Contains(hostname, "index.docker.io") {
|
|
||||||
return "", "", fmt.Errorf("Invalid repository name, try \"%s\" instead", reposName)
|
|
||||||
}
|
|
||||||
if err := validateRepositoryName(reposName); err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return hostname, reposName, nil
|
// Return any configured index info, first.
|
||||||
|
if index, ok := config.IndexConfigs[indexName]; ok {
|
||||||
|
return index, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct a non-configured index info.
|
||||||
|
index := &IndexInfo{
|
||||||
|
Name: indexName,
|
||||||
|
Mirrors: make([]string, 0),
|
||||||
|
Official: false,
|
||||||
|
}
|
||||||
|
index.Secure = config.isSecureIndex(indexName)
|
||||||
|
return index, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateNoSchema(reposName string) error {
|
||||||
|
if strings.Contains(reposName, "://") {
|
||||||
|
// It cannot contain a scheme!
|
||||||
|
return ErrInvalidRepositoryName
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitReposName breaks a reposName into an index name and remote name
|
||||||
|
func splitReposName(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 = IndexServerName()
|
||||||
|
remoteName = reposName
|
||||||
|
} else {
|
||||||
|
indexName = nameParts[0]
|
||||||
|
remoteName = nameParts[1]
|
||||||
|
}
|
||||||
|
return indexName, remoteName
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRepositoryInfo validates and breaks down a repository name into a RepositoryInfo
|
||||||
|
func NewRepositoryInfo(config *ServiceConfig, reposName string) (*RepositoryInfo, error) {
|
||||||
|
if err := validateNoSchema(reposName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
indexName, remoteName := splitReposName(reposName)
|
||||||
|
if err := validateRemoteName(remoteName); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
repoInfo := &RepositoryInfo{
|
||||||
|
RemoteName: remoteName,
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
repoInfo.Index, err = NewIndexInfo(config, indexName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if repoInfo.Index.Official {
|
||||||
|
normalizedName := repoInfo.RemoteName
|
||||||
|
if strings.HasPrefix(normalizedName, "library/") {
|
||||||
|
// If pull "library/foo", it's stored locally under "foo"
|
||||||
|
normalizedName = strings.SplitN(normalizedName, "/", 2)[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
repoInfo.LocalName = normalizedName
|
||||||
|
repoInfo.RemoteName = normalizedName
|
||||||
|
// If the normalized name does not contain a '/' (e.g. "foo")
|
||||||
|
// then it is an official repo.
|
||||||
|
if strings.IndexRune(normalizedName, '/') == -1 {
|
||||||
|
repoInfo.Official = true
|
||||||
|
// Fix up remote name for official repos.
|
||||||
|
repoInfo.RemoteName = "library/" + normalizedName
|
||||||
|
}
|
||||||
|
|
||||||
|
// *TODO: Prefix this with 'docker.io/'.
|
||||||
|
repoInfo.CanonicalName = repoInfo.LocalName
|
||||||
|
} else {
|
||||||
|
// *TODO: Decouple index name from hostname (via registry configuration?)
|
||||||
|
repoInfo.LocalName = repoInfo.Index.Name + "/" + repoInfo.RemoteName
|
||||||
|
repoInfo.CanonicalName = repoInfo.LocalName
|
||||||
|
}
|
||||||
|
return repoInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateRepositoryName validates a repository name
|
||||||
|
func ValidateRepositoryName(reposName string) error {
|
||||||
|
var err error
|
||||||
|
if err = validateNoSchema(reposName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
indexName, remoteName := splitReposName(reposName)
|
||||||
|
if _, err = ValidateIndexName(indexName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return validateRemoteName(remoteName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseRepositoryInfo performs the breakdown of a repository name into a RepositoryInfo, but
|
||||||
|
// lacks registry configuration.
|
||||||
|
func ParseRepositoryInfo(reposName string) (*RepositoryInfo, error) {
|
||||||
|
return NewRepositoryInfo(emptyServiceConfig, reposName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeLocalName transforms a repository name into a normalize LocalName
|
||||||
|
// Passes through the name without transformation on error (image id, etc)
|
||||||
|
func NormalizeLocalName(name string) string {
|
||||||
|
repoInfo, err := ParseRepositoryInfo(name)
|
||||||
|
if err != nil {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return repoInfo.LocalName
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (index *IndexInfo) GetAuthConfigKey() string {
|
||||||
|
if index.Official {
|
||||||
|
return IndexServerAddress()
|
||||||
|
}
|
||||||
|
return index.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSearchTerm special-cases using local name for official index, and
|
||||||
|
// remote name for private indexes.
|
||||||
|
func (repoInfo *RepositoryInfo) GetSearchTerm() string {
|
||||||
|
if repoInfo.Index.Official {
|
||||||
|
return repoInfo.LocalName
|
||||||
|
}
|
||||||
|
return repoInfo.RemoteName
|
||||||
}
|
}
|
||||||
|
|
||||||
func trustedLocation(req *http.Request) bool {
|
func trustedLocation(req *http.Request) bool {
|
||||||
|
|
|
@ -15,15 +15,16 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/opts"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
|
||||||
log "github.com/Sirupsen/logrus"
|
log "github.com/Sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
testHTTPServer *httptest.Server
|
testHTTPServer *httptest.Server
|
||||||
insecureRegistries []string
|
testHTTPSServer *httptest.Server
|
||||||
testLayers = map[string]map[string]string{
|
testLayers = map[string]map[string]string{
|
||||||
"77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20": {
|
"77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20": {
|
||||||
"json": `{"id":"77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20",
|
"json": `{"id":"77dbf71da1d00e3fbddc480176eac8994025630c6590d11cfc8fe1209c2a1d20",
|
||||||
"comment":"test base image","created":"2013-03-23T12:53:11.10432-07:00",
|
"comment":"test base image","created":"2013-03-23T12:53:11.10432-07:00",
|
||||||
|
@ -86,6 +87,7 @@ var (
|
||||||
"": {net.ParseIP("0.0.0.0")},
|
"": {net.ParseIP("0.0.0.0")},
|
||||||
"localhost": {net.ParseIP("127.0.0.1"), net.ParseIP("::1")},
|
"localhost": {net.ParseIP("127.0.0.1"), net.ParseIP("::1")},
|
||||||
"example.com": {net.ParseIP("42.42.42.42")},
|
"example.com": {net.ParseIP("42.42.42.42")},
|
||||||
|
"other.com": {net.ParseIP("43.43.43.43")},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -108,11 +110,7 @@ func init() {
|
||||||
r.HandleFunc("/v2/version", handlerGetPing).Methods("GET")
|
r.HandleFunc("/v2/version", handlerGetPing).Methods("GET")
|
||||||
|
|
||||||
testHTTPServer = httptest.NewServer(handlerAccessLog(r))
|
testHTTPServer = httptest.NewServer(handlerAccessLog(r))
|
||||||
URL, err := url.Parse(testHTTPServer.URL)
|
testHTTPSServer = httptest.NewTLSServer(handlerAccessLog(r))
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
insecureRegistries = []string{URL.Host}
|
|
||||||
|
|
||||||
// override net.LookupIP
|
// override net.LookupIP
|
||||||
lookupIP = func(host string) ([]net.IP, error) {
|
lookupIP = func(host string) ([]net.IP, error) {
|
||||||
|
@ -146,6 +144,52 @@ func makeURL(req string) string {
|
||||||
return testHTTPServer.URL + req
|
return testHTTPServer.URL + req
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeHttpsURL(req string) string {
|
||||||
|
return testHTTPSServer.URL + req
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeIndex(req string) *IndexInfo {
|
||||||
|
index := &IndexInfo{
|
||||||
|
Name: makeURL(req),
|
||||||
|
}
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeHttpsIndex(req string) *IndexInfo {
|
||||||
|
index := &IndexInfo{
|
||||||
|
Name: makeHttpsURL(req),
|
||||||
|
}
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
func makePublicIndex() *IndexInfo {
|
||||||
|
index := &IndexInfo{
|
||||||
|
Name: IndexServerAddress(),
|
||||||
|
Secure: true,
|
||||||
|
Official: true,
|
||||||
|
}
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeServiceConfig(mirrors []string, insecure_registries []string) *ServiceConfig {
|
||||||
|
options := &Options{
|
||||||
|
Mirrors: opts.NewListOpts(nil),
|
||||||
|
InsecureRegistries: opts.NewListOpts(nil),
|
||||||
|
}
|
||||||
|
if mirrors != nil {
|
||||||
|
for _, mirror := range mirrors {
|
||||||
|
options.Mirrors.Set(mirror)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if insecure_registries != nil {
|
||||||
|
for _, insecure_registries := range insecure_registries {
|
||||||
|
options.InsecureRegistries.Set(insecure_registries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return NewServiceConfig(options)
|
||||||
|
}
|
||||||
|
|
||||||
func writeHeaders(w http.ResponseWriter) {
|
func writeHeaders(w http.ResponseWriter) {
|
||||||
h := w.Header()
|
h := w.Header()
|
||||||
h.Add("Server", "docker-tests/mock")
|
h.Add("Server", "docker-tests/mock")
|
||||||
|
@ -193,6 +237,40 @@ func assertEqual(t *testing.T, a interface{}, b interface{}, message string) {
|
||||||
t.Fatal(message)
|
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 {
|
func requiresAuth(w http.ResponseWriter, r *http.Request) bool {
|
||||||
writeCookie := func() {
|
writeCookie := func() {
|
||||||
value := fmt.Sprintf("FAKE-SESSION-%d", time.Now().UnixNano())
|
value := fmt.Sprintf("FAKE-SESSION-%d", time.Now().UnixNano())
|
||||||
|
@ -271,6 +349,7 @@ func handlerGetDeleteTags(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
repositoryName := mux.Vars(r)["repository"]
|
repositoryName := mux.Vars(r)["repository"]
|
||||||
|
repositoryName = NormalizeLocalName(repositoryName)
|
||||||
tags, exists := testRepositories[repositoryName]
|
tags, exists := testRepositories[repositoryName]
|
||||||
if !exists {
|
if !exists {
|
||||||
apiError(w, "Repository not found", 404)
|
apiError(w, "Repository not found", 404)
|
||||||
|
@ -290,6 +369,7 @@ func handlerGetTag(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
repositoryName := vars["repository"]
|
repositoryName := vars["repository"]
|
||||||
|
repositoryName = NormalizeLocalName(repositoryName)
|
||||||
tagName := vars["tag"]
|
tagName := vars["tag"]
|
||||||
tags, exists := testRepositories[repositoryName]
|
tags, exists := testRepositories[repositoryName]
|
||||||
if !exists {
|
if !exists {
|
||||||
|
@ -310,6 +390,7 @@ func handlerPutTag(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
repositoryName := vars["repository"]
|
repositoryName := vars["repository"]
|
||||||
|
repositoryName = NormalizeLocalName(repositoryName)
|
||||||
tagName := vars["tag"]
|
tagName := vars["tag"]
|
||||||
tags, exists := testRepositories[repositoryName]
|
tags, exists := testRepositories[repositoryName]
|
||||||
if !exists {
|
if !exists {
|
||||||
|
|
|
@ -21,7 +21,7 @@ const (
|
||||||
|
|
||||||
func spawnTestRegistrySession(t *testing.T) *Session {
|
func spawnTestRegistrySession(t *testing.T) *Session {
|
||||||
authConfig := &AuthConfig{}
|
authConfig := &AuthConfig{}
|
||||||
endpoint, err := NewEndpoint(makeURL("/v1/"), insecureRegistries)
|
endpoint, err := NewEndpoint(makeIndex("/v1/"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -32,16 +32,139 @@ func spawnTestRegistrySession(t *testing.T) *Session {
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPublicSession(t *testing.T) {
|
||||||
|
authConfig := &AuthConfig{}
|
||||||
|
|
||||||
|
getSessionDecorators := func(index *IndexInfo) int {
|
||||||
|
endpoint, err := NewEndpoint(index)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
r, err := NewSession(authConfig, utils.NewHTTPRequestFactory(), endpoint, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return len(r.reqFactory.GetDecorators())
|
||||||
|
}
|
||||||
|
|
||||||
|
decorators := getSessionDecorators(makeIndex("/v1/"))
|
||||||
|
assertEqual(t, decorators, 0, "Expected no decorator on http session")
|
||||||
|
|
||||||
|
decorators = getSessionDecorators(makeHttpsIndex("/v1/"))
|
||||||
|
assertNotEqual(t, decorators, 0, "Expected decorator on https session")
|
||||||
|
|
||||||
|
decorators = getSessionDecorators(makePublicIndex())
|
||||||
|
assertEqual(t, decorators, 0, "Expected no decorator on public session")
|
||||||
|
}
|
||||||
|
|
||||||
func TestPingRegistryEndpoint(t *testing.T) {
|
func TestPingRegistryEndpoint(t *testing.T) {
|
||||||
ep, err := NewEndpoint(makeURL("/v1/"), insecureRegistries)
|
testPing := func(index *IndexInfo, expectedStandalone bool, assertMessage string) {
|
||||||
if err != nil {
|
ep, err := NewEndpoint(index)
|
||||||
t.Fatal(err)
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
regInfo, err := ep.Ping()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEqual(t, regInfo.Standalone, expectedStandalone, assertMessage)
|
||||||
}
|
}
|
||||||
regInfo, err := ep.Ping()
|
|
||||||
if err != nil {
|
testPing(makeIndex("/v1/"), true, "Expected standalone to be true (default)")
|
||||||
t.Fatal(err)
|
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 *IndexInfo) *Endpoint {
|
||||||
|
endpoint, err := NewEndpoint(index)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
assertInsecureIndex := func(index *IndexInfo) {
|
||||||
|
index.Secure = true
|
||||||
|
_, err := NewEndpoint(index)
|
||||||
|
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 *IndexInfo) {
|
||||||
|
index.Secure = true
|
||||||
|
_, err := NewEndpoint(index)
|
||||||
|
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 := &IndexInfo{}
|
||||||
|
index.Name = makeURL("/v1/")
|
||||||
|
endpoint := expandEndpoint(index)
|
||||||
|
assertEqual(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name)
|
||||||
|
if endpoint.Version != APIVersion1 {
|
||||||
|
t.Fatal("Expected endpoint to be v1")
|
||||||
|
}
|
||||||
|
assertInsecureIndex(index)
|
||||||
|
|
||||||
|
index.Name = makeURL("")
|
||||||
|
endpoint = expandEndpoint(index)
|
||||||
|
assertEqual(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/")
|
||||||
|
if endpoint.Version != APIVersion1 {
|
||||||
|
t.Fatal("Expected endpoint to be 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/")
|
||||||
|
if endpoint.Version != APIVersion1 {
|
||||||
|
t.Fatal("Expected endpoint to be v1")
|
||||||
|
}
|
||||||
|
assertInsecureIndex(index)
|
||||||
|
|
||||||
|
index.Name = makeHttpsURL("/v1/")
|
||||||
|
endpoint = expandEndpoint(index)
|
||||||
|
assertEqual(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name)
|
||||||
|
if endpoint.Version != APIVersion1 {
|
||||||
|
t.Fatal("Expected endpoint to be v1")
|
||||||
|
}
|
||||||
|
assertSecureIndex(index)
|
||||||
|
|
||||||
|
index.Name = makeHttpsURL("")
|
||||||
|
endpoint = expandEndpoint(index)
|
||||||
|
assertEqual(t, endpoint.String(), index.Name+"/v1/", index.Name+": Expected endpoint to be "+index.Name+"/v1/")
|
||||||
|
if endpoint.Version != APIVersion1 {
|
||||||
|
t.Fatal("Expected endpoint to be 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/")
|
||||||
|
if endpoint.Version != APIVersion1 {
|
||||||
|
t.Fatal("Expected endpoint to be 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 := NewEndpoint(index)
|
||||||
|
checkNotEqual(t, err, nil, "Expected error while expanding bad endpoint")
|
||||||
}
|
}
|
||||||
assertEqual(t, regInfo.Standalone, true, "Expected standalone to be true (default)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetRemoteHistory(t *testing.T) {
|
func TestGetRemoteHistory(t *testing.T) {
|
||||||
|
@ -156,30 +279,413 @@ func TestPushImageLayerRegistry(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestResolveRepositoryName(t *testing.T) {
|
func TestValidateRepositoryName(t *testing.T) {
|
||||||
_, _, err := ResolveRepositoryName("https://github.com/docker/docker")
|
validRepoNames := []string{
|
||||||
assertEqual(t, err, ErrInvalidRepositoryName, "Expected error invalid repo name")
|
"docker/docker",
|
||||||
ep, repo, err := ResolveRepositoryName("fooo/bar")
|
"library/debian",
|
||||||
if err != nil {
|
"debian",
|
||||||
t.Fatal(err)
|
"docker.io/docker/docker",
|
||||||
|
"docker.io/library/debian",
|
||||||
|
"docker.io/debian",
|
||||||
|
"index.docker.io/docker/docker",
|
||||||
|
"index.docker.io/library/debian",
|
||||||
|
"index.docker.io/debian",
|
||||||
|
"127.0.0.1:5000/docker/docker",
|
||||||
|
"127.0.0.1:5000/library/debian",
|
||||||
|
"127.0.0.1:5000/debian",
|
||||||
|
"thisisthesongthatneverendsitgoesonandonandonthisisthesongthatnev",
|
||||||
|
}
|
||||||
|
invalidRepoNames := []string{
|
||||||
|
"https://github.com/docker/docker",
|
||||||
|
"docker/Docker",
|
||||||
|
"docker///docker",
|
||||||
|
"docker.io/docker/Docker",
|
||||||
|
"docker.io/docker///docker",
|
||||||
|
"1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a",
|
||||||
|
"docker.io/1a3f5e7d9c1b3a5f7e9d1c3b5a7f9e1d3c5b7a9f1e3d5d7c9b1a3f5e7d9c1b3a",
|
||||||
}
|
}
|
||||||
assertEqual(t, ep, IndexServerAddress(), "Expected endpoint to be index server address")
|
|
||||||
assertEqual(t, repo, "fooo/bar", "Expected resolved repo to be foo/bar")
|
|
||||||
|
|
||||||
u := makeURL("")[7:]
|
for _, name := range invalidRepoNames {
|
||||||
ep, repo, err = ResolveRepositoryName(u + "/private/moonbase")
|
err := ValidateRepositoryName(name)
|
||||||
if err != nil {
|
assertNotEqual(t, err, nil, "Expected invalid repo name: "+name)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
}
|
||||||
assertEqual(t, ep, u, "Expected endpoint to be "+u)
|
|
||||||
assertEqual(t, repo, "private/moonbase", "Expected endpoint to be private/moonbase")
|
|
||||||
|
|
||||||
ep, repo, err = ResolveRepositoryName("ubuntu-12.04-base")
|
for _, name := range validRepoNames {
|
||||||
if err != nil {
|
err := ValidateRepositoryName(name)
|
||||||
t.Fatal(err)
|
assertEqual(t, err, nil, "Expected valid repo name: "+name)
|
||||||
}
|
}
|
||||||
assertEqual(t, ep, IndexServerAddress(), "Expected endpoint to be "+IndexServerAddress())
|
|
||||||
assertEqual(t, repo, "ubuntu-12.04-base", "Expected endpoint to be ubuntu-12.04-base")
|
err := ValidateRepositoryName(invalidRepoNames[0])
|
||||||
|
assertEqual(t, err, ErrInvalidRepositoryName, "Expected ErrInvalidRepositoryName: "+invalidRepoNames[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseRepositoryInfo(t *testing.T) {
|
||||||
|
expectedRepoInfos := map[string]RepositoryInfo{
|
||||||
|
"fooo/bar": {
|
||||||
|
Index: &IndexInfo{
|
||||||
|
Name: IndexServerName(),
|
||||||
|
Official: true,
|
||||||
|
},
|
||||||
|
RemoteName: "fooo/bar",
|
||||||
|
LocalName: "fooo/bar",
|
||||||
|
CanonicalName: "fooo/bar",
|
||||||
|
Official: false,
|
||||||
|
},
|
||||||
|
"library/ubuntu": {
|
||||||
|
Index: &IndexInfo{
|
||||||
|
Name: IndexServerName(),
|
||||||
|
Official: true,
|
||||||
|
},
|
||||||
|
RemoteName: "library/ubuntu",
|
||||||
|
LocalName: "ubuntu",
|
||||||
|
CanonicalName: "ubuntu",
|
||||||
|
Official: true,
|
||||||
|
},
|
||||||
|
"nonlibrary/ubuntu": {
|
||||||
|
Index: &IndexInfo{
|
||||||
|
Name: IndexServerName(),
|
||||||
|
Official: true,
|
||||||
|
},
|
||||||
|
RemoteName: "nonlibrary/ubuntu",
|
||||||
|
LocalName: "nonlibrary/ubuntu",
|
||||||
|
CanonicalName: "nonlibrary/ubuntu",
|
||||||
|
Official: false,
|
||||||
|
},
|
||||||
|
"ubuntu": {
|
||||||
|
Index: &IndexInfo{
|
||||||
|
Name: IndexServerName(),
|
||||||
|
Official: true,
|
||||||
|
},
|
||||||
|
RemoteName: "library/ubuntu",
|
||||||
|
LocalName: "ubuntu",
|
||||||
|
CanonicalName: "ubuntu",
|
||||||
|
Official: true,
|
||||||
|
},
|
||||||
|
"other/library": {
|
||||||
|
Index: &IndexInfo{
|
||||||
|
Name: IndexServerName(),
|
||||||
|
Official: true,
|
||||||
|
},
|
||||||
|
RemoteName: "other/library",
|
||||||
|
LocalName: "other/library",
|
||||||
|
CanonicalName: "other/library",
|
||||||
|
Official: false,
|
||||||
|
},
|
||||||
|
"127.0.0.1:8000/private/moonbase": {
|
||||||
|
Index: &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: &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: &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: &IndexInfo{
|
||||||
|
Name: "localhost:8000",
|
||||||
|
Official: false,
|
||||||
|
},
|
||||||
|
RemoteName: "privatebase",
|
||||||
|
LocalName: "localhost:8000/privatebase",
|
||||||
|
CanonicalName: "localhost:8000/privatebase",
|
||||||
|
Official: false,
|
||||||
|
},
|
||||||
|
"example.com/private/moonbase": {
|
||||||
|
Index: &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: &IndexInfo{
|
||||||
|
Name: "example.com",
|
||||||
|
Official: false,
|
||||||
|
},
|
||||||
|
RemoteName: "privatebase",
|
||||||
|
LocalName: "example.com/privatebase",
|
||||||
|
CanonicalName: "example.com/privatebase",
|
||||||
|
Official: false,
|
||||||
|
},
|
||||||
|
"example.com:8000/private/moonbase": {
|
||||||
|
Index: &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: &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: &IndexInfo{
|
||||||
|
Name: "localhost",
|
||||||
|
Official: false,
|
||||||
|
},
|
||||||
|
RemoteName: "private/moonbase",
|
||||||
|
LocalName: "localhost/private/moonbase",
|
||||||
|
CanonicalName: "localhost/private/moonbase",
|
||||||
|
Official: false,
|
||||||
|
},
|
||||||
|
"localhost/privatebase": {
|
||||||
|
Index: &IndexInfo{
|
||||||
|
Name: "localhost",
|
||||||
|
Official: false,
|
||||||
|
},
|
||||||
|
RemoteName: "privatebase",
|
||||||
|
LocalName: "localhost/privatebase",
|
||||||
|
CanonicalName: "localhost/privatebase",
|
||||||
|
Official: false,
|
||||||
|
},
|
||||||
|
IndexServerName() + "/public/moonbase": {
|
||||||
|
Index: &IndexInfo{
|
||||||
|
Name: IndexServerName(),
|
||||||
|
Official: true,
|
||||||
|
},
|
||||||
|
RemoteName: "public/moonbase",
|
||||||
|
LocalName: "public/moonbase",
|
||||||
|
CanonicalName: "public/moonbase",
|
||||||
|
Official: false,
|
||||||
|
},
|
||||||
|
"index." + IndexServerName() + "/public/moonbase": {
|
||||||
|
Index: &IndexInfo{
|
||||||
|
Name: IndexServerName(),
|
||||||
|
Official: true,
|
||||||
|
},
|
||||||
|
RemoteName: "public/moonbase",
|
||||||
|
LocalName: "public/moonbase",
|
||||||
|
CanonicalName: "public/moonbase",
|
||||||
|
Official: false,
|
||||||
|
},
|
||||||
|
IndexServerName() + "/public/moonbase": {
|
||||||
|
Index: &IndexInfo{
|
||||||
|
Name: IndexServerName(),
|
||||||
|
Official: true,
|
||||||
|
},
|
||||||
|
RemoteName: "public/moonbase",
|
||||||
|
LocalName: "public/moonbase",
|
||||||
|
CanonicalName: "public/moonbase",
|
||||||
|
Official: false,
|
||||||
|
},
|
||||||
|
"ubuntu-12.04-base": {
|
||||||
|
Index: &IndexInfo{
|
||||||
|
Name: IndexServerName(),
|
||||||
|
Official: true,
|
||||||
|
},
|
||||||
|
RemoteName: "library/ubuntu-12.04-base",
|
||||||
|
LocalName: "ubuntu-12.04-base",
|
||||||
|
CanonicalName: "ubuntu-12.04-base",
|
||||||
|
Official: true,
|
||||||
|
},
|
||||||
|
IndexServerName() + "/ubuntu-12.04-base": {
|
||||||
|
Index: &IndexInfo{
|
||||||
|
Name: IndexServerName(),
|
||||||
|
Official: true,
|
||||||
|
},
|
||||||
|
RemoteName: "library/ubuntu-12.04-base",
|
||||||
|
LocalName: "ubuntu-12.04-base",
|
||||||
|
CanonicalName: "ubuntu-12.04-base",
|
||||||
|
Official: true,
|
||||||
|
},
|
||||||
|
IndexServerName() + "/ubuntu-12.04-base": {
|
||||||
|
Index: &IndexInfo{
|
||||||
|
Name: IndexServerName(),
|
||||||
|
Official: true,
|
||||||
|
},
|
||||||
|
RemoteName: "library/ubuntu-12.04-base",
|
||||||
|
LocalName: "ubuntu-12.04-base",
|
||||||
|
CanonicalName: "ubuntu-12.04-base",
|
||||||
|
Official: true,
|
||||||
|
},
|
||||||
|
"index." + IndexServerName() + "/ubuntu-12.04-base": {
|
||||||
|
Index: &IndexInfo{
|
||||||
|
Name: IndexServerName(),
|
||||||
|
Official: true,
|
||||||
|
},
|
||||||
|
RemoteName: "library/ubuntu-12.04-base",
|
||||||
|
LocalName: "ubuntu-12.04-base",
|
||||||
|
CanonicalName: "ubuntu-12.04-base",
|
||||||
|
Official: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for reposName, expectedRepoInfo := range expectedRepoInfos {
|
||||||
|
repoInfo, err := ParseRepositoryInfo(reposName)
|
||||||
|
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.LocalName, expectedRepoInfo.LocalName, reposName)
|
||||||
|
checkEqual(t, repoInfo.CanonicalName, 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]*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(nil)
|
||||||
|
noMirrors := make([]string, 0)
|
||||||
|
expectedIndexInfos := map[string]*IndexInfo{
|
||||||
|
IndexServerName(): {
|
||||||
|
Name: IndexServerName(),
|
||||||
|
Official: true,
|
||||||
|
Secure: true,
|
||||||
|
Mirrors: noMirrors,
|
||||||
|
},
|
||||||
|
"index." + IndexServerName(): {
|
||||||
|
Name: IndexServerName(),
|
||||||
|
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]*IndexInfo{
|
||||||
|
IndexServerName(): {
|
||||||
|
Name: IndexServerName(),
|
||||||
|
Official: true,
|
||||||
|
Secure: true,
|
||||||
|
Mirrors: publicMirrors,
|
||||||
|
},
|
||||||
|
"index." + IndexServerName(): {
|
||||||
|
Name: IndexServerName(),
|
||||||
|
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]*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 TestPushRegistryTag(t *testing.T) {
|
func TestPushRegistryTag(t *testing.T) {
|
||||||
|
@ -232,7 +738,7 @@ func TestSearchRepositories(t *testing.T) {
|
||||||
assertEqual(t, results.Results[0].StarCount, 42, "Expected 'fakeimage' a ot hae 42 stars")
|
assertEqual(t, results.Results[0].StarCount, 42, "Expected 'fakeimage' a ot hae 42 stars")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidRepositoryName(t *testing.T) {
|
func TestValidRemoteName(t *testing.T) {
|
||||||
validRepositoryNames := []string{
|
validRepositoryNames := []string{
|
||||||
// Sanity check.
|
// Sanity check.
|
||||||
"docker/docker",
|
"docker/docker",
|
||||||
|
@ -247,7 +753,7 @@ func TestValidRepositoryName(t *testing.T) {
|
||||||
"____/____",
|
"____/____",
|
||||||
}
|
}
|
||||||
for _, repositoryName := range validRepositoryNames {
|
for _, repositoryName := range validRepositoryNames {
|
||||||
if err := validateRepositoryName(repositoryName); err != nil {
|
if err := validateRemoteName(repositoryName); err != nil {
|
||||||
t.Errorf("Repository name should be valid: %v. Error: %v", repositoryName, err)
|
t.Errorf("Repository name should be valid: %v. Error: %v", repositoryName, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -277,7 +783,7 @@ func TestValidRepositoryName(t *testing.T) {
|
||||||
"docker/",
|
"docker/",
|
||||||
}
|
}
|
||||||
for _, repositoryName := range invalidRepositoryNames {
|
for _, repositoryName := range invalidRepositoryNames {
|
||||||
if err := validateRepositoryName(repositoryName); err == nil {
|
if err := validateRemoteName(repositoryName); err == nil {
|
||||||
t.Errorf("Repository name should be invalid: %v", repositoryName)
|
t.Errorf("Repository name should be invalid: %v", repositoryName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -350,13 +856,13 @@ func TestAddRequiredHeadersToRedirectedRequests(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIsSecure(t *testing.T) {
|
func TestIsSecureIndex(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
addr string
|
addr string
|
||||||
insecureRegistries []string
|
insecureRegistries []string
|
||||||
expected bool
|
expected bool
|
||||||
}{
|
}{
|
||||||
{IndexServerURL.Host, nil, true},
|
{IndexServerName(), nil, true},
|
||||||
{"example.com", []string{}, true},
|
{"example.com", []string{}, true},
|
||||||
{"example.com", []string{"example.com"}, false},
|
{"example.com", []string{"example.com"}, false},
|
||||||
{"localhost", []string{"localhost:5000"}, false},
|
{"localhost", []string{"localhost:5000"}, false},
|
||||||
|
@ -383,10 +889,9 @@ func TestIsSecure(t *testing.T) {
|
||||||
{"invalid.domain.com:5000", []string{"invalid.domain.com:5000"}, false},
|
{"invalid.domain.com:5000", []string{"invalid.domain.com:5000"}, false},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
// TODO: remove this once we remove localhost insecure by default
|
config := makeServiceConfig(nil, tt.insecureRegistries)
|
||||||
insecureRegistries := append(tt.insecureRegistries, "127.0.0.0/8")
|
if sec := config.isSecureIndex(tt.addr); sec != tt.expected {
|
||||||
if sec, err := isSecure(tt.addr, insecureRegistries); err != nil || sec != tt.expected {
|
t.Errorf("isSecureIndex failed for %q %v, expected %v got %v", tt.addr, tt.insecureRegistries, tt.expected, sec)
|
||||||
t.Fatalf("isSecure failed for %q %v, expected %v got %v. Error: %v", tt.addr, insecureRegistries, tt.expected, sec, err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
119
docs/service.go
119
docs/service.go
|
@ -13,14 +13,14 @@ import (
|
||||||
// 'pull': Download images from any registry (TODO)
|
// 'pull': Download images from any registry (TODO)
|
||||||
// 'push': Upload images to any registry (TODO)
|
// 'push': Upload images to any registry (TODO)
|
||||||
type Service struct {
|
type Service struct {
|
||||||
insecureRegistries []string
|
Config *ServiceConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService returns a new instance of Service ready to be
|
// NewService returns a new instance of Service ready to be
|
||||||
// installed no an engine.
|
// installed no an engine.
|
||||||
func NewService(insecureRegistries []string) *Service {
|
func NewService(options *Options) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
insecureRegistries: insecureRegistries,
|
Config: NewServiceConfig(options),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,6 +28,9 @@ func NewService(insecureRegistries []string) *Service {
|
||||||
func (s *Service) Install(eng *engine.Engine) error {
|
func (s *Service) Install(eng *engine.Engine) error {
|
||||||
eng.Register("auth", s.Auth)
|
eng.Register("auth", s.Auth)
|
||||||
eng.Register("search", s.Search)
|
eng.Register("search", s.Search)
|
||||||
|
eng.Register("resolve_repository", s.ResolveRepository)
|
||||||
|
eng.Register("resolve_index", s.ResolveIndex)
|
||||||
|
eng.Register("registry_config", s.GetRegistryConfig)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,15 +42,18 @@ func (s *Service) Auth(job *engine.Job) engine.Status {
|
||||||
|
|
||||||
job.GetenvJson("authConfig", authConfig)
|
job.GetenvJson("authConfig", authConfig)
|
||||||
|
|
||||||
if addr := authConfig.ServerAddress; addr != "" && addr != IndexServerAddress() {
|
if authConfig.ServerAddress != "" {
|
||||||
endpoint, err := NewEndpoint(addr, s.insecureRegistries)
|
index, err := ResolveIndexInfo(job, authConfig.ServerAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return job.Error(err)
|
return job.Error(err)
|
||||||
}
|
}
|
||||||
if _, err := endpoint.Ping(); err != nil {
|
if !index.Official {
|
||||||
return job.Error(err)
|
endpoint, err := NewEndpoint(index)
|
||||||
|
if err != nil {
|
||||||
|
return job.Error(err)
|
||||||
|
}
|
||||||
|
authConfig.ServerAddress = endpoint.String()
|
||||||
}
|
}
|
||||||
authConfig.ServerAddress = endpoint.String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
status, err := Login(authConfig, HTTPRequestFactory(nil))
|
status, err := Login(authConfig, HTTPRequestFactory(nil))
|
||||||
|
@ -87,12 +93,12 @@ func (s *Service) Search(job *engine.Job) engine.Status {
|
||||||
job.GetenvJson("authConfig", authConfig)
|
job.GetenvJson("authConfig", authConfig)
|
||||||
job.GetenvJson("metaHeaders", metaHeaders)
|
job.GetenvJson("metaHeaders", metaHeaders)
|
||||||
|
|
||||||
hostname, term, err := ResolveRepositoryName(term)
|
repoInfo, err := ResolveRepositoryInfo(job, term)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return job.Error(err)
|
return job.Error(err)
|
||||||
}
|
}
|
||||||
|
// *TODO: Search multiple indexes.
|
||||||
endpoint, err := NewEndpoint(hostname, s.insecureRegistries)
|
endpoint, err := repoInfo.GetEndpoint()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return job.Error(err)
|
return job.Error(err)
|
||||||
}
|
}
|
||||||
|
@ -100,7 +106,7 @@ func (s *Service) Search(job *engine.Job) engine.Status {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return job.Error(err)
|
return job.Error(err)
|
||||||
}
|
}
|
||||||
results, err := r.SearchRepositories(term)
|
results, err := r.SearchRepositories(repoInfo.GetSearchTerm())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return job.Error(err)
|
return job.Error(err)
|
||||||
}
|
}
|
||||||
|
@ -116,3 +122,92 @@ func (s *Service) Search(job *engine.Job) engine.Status {
|
||||||
}
|
}
|
||||||
return engine.StatusOK
|
return engine.StatusOK
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResolveRepository splits a repository name into its components
|
||||||
|
// and configuration of the associated registry.
|
||||||
|
func (s *Service) ResolveRepository(job *engine.Job) engine.Status {
|
||||||
|
var (
|
||||||
|
reposName = job.Args[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
repoInfo, err := NewRepositoryInfo(s.Config, reposName)
|
||||||
|
if err != nil {
|
||||||
|
return job.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out := engine.Env{}
|
||||||
|
err = out.SetJson("repository", repoInfo)
|
||||||
|
if err != nil {
|
||||||
|
return job.Error(err)
|
||||||
|
}
|
||||||
|
out.WriteTo(job.Stdout)
|
||||||
|
|
||||||
|
return engine.StatusOK
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience wrapper for calling resolve_repository Job from a running job.
|
||||||
|
func ResolveRepositoryInfo(jobContext *engine.Job, reposName string) (*RepositoryInfo, error) {
|
||||||
|
job := jobContext.Eng.Job("resolve_repository", reposName)
|
||||||
|
env, err := job.Stdout.AddEnv()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := job.Run(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
info := RepositoryInfo{}
|
||||||
|
if err := env.GetJson("repository", &info); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveIndex takes indexName and returns index info
|
||||||
|
func (s *Service) ResolveIndex(job *engine.Job) engine.Status {
|
||||||
|
var (
|
||||||
|
indexName = job.Args[0]
|
||||||
|
)
|
||||||
|
|
||||||
|
index, err := NewIndexInfo(s.Config, indexName)
|
||||||
|
if err != nil {
|
||||||
|
return job.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
out := engine.Env{}
|
||||||
|
err = out.SetJson("index", index)
|
||||||
|
if err != nil {
|
||||||
|
return job.Error(err)
|
||||||
|
}
|
||||||
|
out.WriteTo(job.Stdout)
|
||||||
|
|
||||||
|
return engine.StatusOK
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convenience wrapper for calling resolve_index Job from a running job.
|
||||||
|
func ResolveIndexInfo(jobContext *engine.Job, indexName string) (*IndexInfo, error) {
|
||||||
|
job := jobContext.Eng.Job("resolve_index", indexName)
|
||||||
|
env, err := job.Stdout.AddEnv()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := job.Run(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
info := IndexInfo{}
|
||||||
|
if err := env.GetJson("index", &info); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRegistryConfig returns current registry configuration.
|
||||||
|
func (s *Service) GetRegistryConfig(job *engine.Job) engine.Status {
|
||||||
|
out := engine.Env{}
|
||||||
|
err := out.SetJson("config", s.Config)
|
||||||
|
if err != nil {
|
||||||
|
return job.Error(err)
|
||||||
|
}
|
||||||
|
out.WriteTo(job.Stdout)
|
||||||
|
|
||||||
|
return engine.StatusOK
|
||||||
|
}
|
||||||
|
|
|
@ -65,3 +65,44 @@ const (
|
||||||
APIVersion1 = iota + 1
|
APIVersion1 = iota + 1
|
||||||
APIVersion2
|
APIVersion2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// RepositoryInfo Examples:
|
||||||
|
// {
|
||||||
|
// "Index" : {
|
||||||
|
// "Name" : "docker.io",
|
||||||
|
// "Mirrors" : ["https://registry-2.docker.io/v1/", "https://registry-3.docker.io/v1/"],
|
||||||
|
// "Secure" : true,
|
||||||
|
// "Official" : true,
|
||||||
|
// },
|
||||||
|
// "RemoteName" : "library/debian",
|
||||||
|
// "LocalName" : "debian",
|
||||||
|
// "CanonicalName" : "docker.io/debian"
|
||||||
|
// "Official" : true,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// {
|
||||||
|
// "Index" : {
|
||||||
|
// "Name" : "127.0.0.1:5000",
|
||||||
|
// "Mirrors" : [],
|
||||||
|
// "Secure" : false,
|
||||||
|
// "Official" : false,
|
||||||
|
// },
|
||||||
|
// "RemoteName" : "user/repo",
|
||||||
|
// "LocalName" : "127.0.0.1:5000/user/repo",
|
||||||
|
// "CanonicalName" : "127.0.0.1:5000/user/repo",
|
||||||
|
// "Official" : false,
|
||||||
|
// }
|
||||||
|
type IndexInfo struct {
|
||||||
|
Name string
|
||||||
|
Mirrors []string
|
||||||
|
Secure bool
|
||||||
|
Official bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type RepositoryInfo struct {
|
||||||
|
Index *IndexInfo
|
||||||
|
RemoteName string
|
||||||
|
LocalName string
|
||||||
|
CanonicalName string
|
||||||
|
Official bool
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue