mirror of https://github.com/docker/cli.git
add internal fork of docker/docker/registry
This adds an internal fork of [github.com/docker/docker/registry], taken at commit [moby@f651a5d]. Git history was not preserved in this fork, but can be found using the URLs provided. This fork was created to remove the dependency on the "Moby" codebase, and because the CLI only needs a subset of its features. The original package was written specifically for use in the daemon code, and includes functionality that cannot be used in the CLI. [github.com/docker/docker/registry]: https://pkg.go.dev/github.com/docker/docker@v28.3.2+incompatible/registry [moby@49306c6]:49306c607b/registrySigned-off-by: Sebastiaan van Stijn <github@gone.nl> (cherry picked from commitf6b90bc253) Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
parent
93a51c39f4
commit
8b9baffdf7
|
|
@ -16,11 +16,11 @@ import (
|
|||
"github.com/docker/cli/cli/command/completion"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/cli/internal/tui"
|
||||
"github.com/docker/docker/api/types/auxprogress"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/morikuni/aec"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ import (
|
|||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/docker/api/types/image"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ import (
|
|||
"github.com/docker/cli/cli/command/image"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/internal/prompt"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/docker/api/types"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import (
|
|||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -15,10 +15,10 @@ import (
|
|||
"github.com/docker/cli/cli/config/configfile"
|
||||
configtypes "github.com/docker/cli/cli/config/types"
|
||||
"github.com/docker/cli/internal/oauth/manager"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/cli/internal/tui"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
|
|
|||
|
|
@ -13,11 +13,11 @@ import (
|
|||
configtypes "github.com/docker/cli/cli/config/types"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/prompt"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/cli/internal/test"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/registry"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
"gotest.tools/v3/fs"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import (
|
|||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/config/credentials"
|
||||
"github.com/docker/cli/internal/oauth/manager"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ import (
|
|||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
|
|
|||
|
|
@ -19,11 +19,11 @@ import (
|
|||
"github.com/docker/cli/cli/debug"
|
||||
flagsHelper "github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/cli/internal/lazyregexp"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/cli/templates"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
"github.com/docker/docker/api/types/system"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/go-units"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/distribution/registry/client/auth"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/manifest/manifestlist"
|
||||
"github.com/docker/distribution/manifest/ocischema"
|
||||
|
|
@ -13,7 +14,6 @@ import (
|
|||
"github.com/docker/distribution/registry/api/errcode"
|
||||
v2 "github.com/docker/distribution/registry/api/v2"
|
||||
distclient "github.com/docker/distribution/registry/client"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ import (
|
|||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/distribution/registry/client/auth"
|
||||
"github.com/docker/distribution/registry/client/auth/challenge"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/docker/go-connections/tlsconfig"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
|
|
|
|||
|
|
@ -11,9 +11,9 @@ import (
|
|||
"github.com/distribution/reference"
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/jsonstream"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/docker/api/types"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/theupdateframework/notary/client"
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ import (
|
|||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/docker/cli/internal/oauth"
|
||||
"github.com/docker/cli/internal/oauth/api"
|
||||
"github.com/docker/cli/internal/registry"
|
||||
"github.com/docker/cli/internal/tui"
|
||||
"github.com/docker/docker/registry"
|
||||
"github.com/morikuni/aec"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
|
|
|
|||
|
|
@ -40,9 +40,9 @@ type staticCredentialStore struct {
|
|||
|
||||
// NewStaticCredentialStore returns a credential store
|
||||
// which always returns the same credential values.
|
||||
func NewStaticCredentialStore(auth *registry.AuthConfig) auth.CredentialStore {
|
||||
func NewStaticCredentialStore(ac *registry.AuthConfig) auth.CredentialStore {
|
||||
return staticCredentialStore{
|
||||
auth: auth,
|
||||
auth: ac,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ func (scs staticCredentialStore) RefreshToken(*url.URL, string) string {
|
|||
return scs.auth.IdentityToken
|
||||
}
|
||||
|
||||
func (scs staticCredentialStore) SetRefreshToken(*url.URL, string, string) {
|
||||
func (staticCredentialStore) SetRefreshToken(*url.URL, string, string) {
|
||||
}
|
||||
|
||||
// loginV2 tries to login to the v2 registry server. The given registry
|
||||
|
|
@ -131,12 +131,15 @@ func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifi
|
|||
// to just its hostname. It is used to match credentials, which may be either
|
||||
// stored as hostname or as hostname including scheme (in legacy configuration
|
||||
// files).
|
||||
func ConvertToHostname(url string) string {
|
||||
stripped := url
|
||||
if strings.HasPrefix(stripped, "http://") {
|
||||
stripped = strings.TrimPrefix(stripped, "http://")
|
||||
} else if strings.HasPrefix(stripped, "https://") {
|
||||
stripped = strings.TrimPrefix(stripped, "https://")
|
||||
func ConvertToHostname(maybeURL string) string {
|
||||
stripped := maybeURL
|
||||
if scheme, remainder, ok := strings.Cut(stripped, "://"); ok {
|
||||
switch scheme {
|
||||
case "http", "https":
|
||||
stripped = remainder
|
||||
default:
|
||||
// unknown, or no scheme; doing nothing for now, as we never did.
|
||||
}
|
||||
}
|
||||
stripped, _, _ = strings.Cut(stripped, "/")
|
||||
return stripped
|
||||
|
|
@ -175,9 +178,9 @@ func (err PingResponseError) Error() string {
|
|||
// PingV2Registry attempts to ping a v2 registry and on success return a
|
||||
// challenge manager for the supported authentication types.
|
||||
// If a response is received but cannot be interpreted, a PingResponseError will be returned.
|
||||
func PingV2Registry(endpoint *url.URL, transport http.RoundTripper) (challenge.Manager, error) {
|
||||
func PingV2Registry(endpoint *url.URL, authTransport http.RoundTripper) (challenge.Manager, error) {
|
||||
pingClient := &http.Client{
|
||||
Transport: transport,
|
||||
Transport: authTransport,
|
||||
Timeout: 15 * time.Second,
|
||||
}
|
||||
endpointStr := strings.TrimRight(endpoint.String(), "/") + "/v2/"
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func buildAuthConfigs() map[string]registry.AuthConfig {
|
||||
authConfigs := map[string]registry.AuthConfig{}
|
||||
|
||||
for _, reg := range []string{"testIndex", IndexServer} {
|
||||
authConfigs[reg] = registry.AuthConfig{
|
||||
Username: "docker-user",
|
||||
Password: "docker-pass",
|
||||
}
|
||||
}
|
||||
|
||||
return authConfigs
|
||||
}
|
||||
|
||||
func TestResolveAuthConfigIndexServer(t *testing.T) {
|
||||
authConfigs := buildAuthConfigs()
|
||||
indexConfig := authConfigs[IndexServer]
|
||||
|
||||
officialIndex := ®istry.IndexInfo{
|
||||
Official: true,
|
||||
}
|
||||
privateIndex := ®istry.IndexInfo{
|
||||
Official: false,
|
||||
}
|
||||
|
||||
resolved := ResolveAuthConfig(authConfigs, officialIndex)
|
||||
assert.Equal(t, resolved, indexConfig, "Expected ResolveAuthConfig to return IndexServer")
|
||||
|
||||
resolved = ResolveAuthConfig(authConfigs, privateIndex)
|
||||
assert.Check(t, resolved != indexConfig, "Expected ResolveAuthConfig to not return IndexServer")
|
||||
}
|
||||
|
||||
func TestResolveAuthConfigFullURL(t *testing.T) {
|
||||
authConfigs := buildAuthConfigs()
|
||||
|
||||
registryAuth := registry.AuthConfig{
|
||||
Username: "foo-user",
|
||||
Password: "foo-pass",
|
||||
}
|
||||
localAuth := registry.AuthConfig{
|
||||
Username: "bar-user",
|
||||
Password: "bar-pass",
|
||||
}
|
||||
officialAuth := registry.AuthConfig{
|
||||
Username: "baz-user",
|
||||
Password: "baz-pass",
|
||||
}
|
||||
authConfigs[IndexServer] = officialAuth
|
||||
|
||||
expectedAuths := map[string]registry.AuthConfig{
|
||||
"registry.example.com": registryAuth,
|
||||
"localhost:8000": localAuth,
|
||||
"example.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/",
|
||||
},
|
||||
"example.com": {
|
||||
"https://example.com/v1/",
|
||||
"http://example.com/v1/",
|
||||
"example.com",
|
||||
"example.com/v1/",
|
||||
},
|
||||
}
|
||||
|
||||
for configKey, registries := range validRegistries {
|
||||
configured, ok := expectedAuths[configKey]
|
||||
if !ok {
|
||||
t.Fail()
|
||||
}
|
||||
index := ®istry.IndexInfo{
|
||||
Name: configKey,
|
||||
}
|
||||
for _, reg := range registries {
|
||||
authConfigs[reg] = configured
|
||||
resolved := ResolveAuthConfig(authConfigs, index)
|
||||
if resolved.Username != configured.Username || resolved.Password != configured.Password {
|
||||
t.Errorf("%s -> %v != %v\n", reg, resolved, configured)
|
||||
}
|
||||
delete(authConfigs, reg)
|
||||
resolved = ResolveAuthConfig(authConfigs, index)
|
||||
if resolved.Username == configured.Username || resolved.Password == configured.Password {
|
||||
t.Errorf("%s -> %v == %v\n", reg, resolved, configured)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
|
||||
//go:build go1.23
|
||||
|
||||
package registry
|
||||
|
||||
import (
|
||||
|
|
@ -6,6 +9,8 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
|
@ -13,8 +18,6 @@ import (
|
|||
"github.com/containerd/log"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"github.com/docker/docker/internal/lazyregexp"
|
||||
"github.com/docker/docker/pkg/homedir"
|
||||
)
|
||||
|
||||
// ServiceOptions holds command line options.
|
||||
|
|
@ -58,52 +61,36 @@ var (
|
|||
Host: DefaultRegistryHost,
|
||||
}
|
||||
|
||||
validHostPortRegex = lazyregexp.New(`^` + reference.DomainRegexp.String() + `$`)
|
||||
|
||||
// certsDir is used to override defaultCertsDir when running with rootlessKit.
|
||||
//
|
||||
// TODO(thaJeztah): change to a sync.OnceValue once we remove [SetCertsDir]
|
||||
// TODO(thaJeztah): certsDir should not be a package variable, but stored in our config, and passed when needed.
|
||||
setCertsDirOnce sync.Once
|
||||
certsDir string
|
||||
validHostPortRegex = sync.OnceValue(func() *regexp.Regexp {
|
||||
return regexp.MustCompile(`^` + reference.DomainRegexp.String() + `$`)
|
||||
})
|
||||
)
|
||||
|
||||
func setCertsDir(dir string) string {
|
||||
setCertsDirOnce.Do(func() {
|
||||
if dir != "" {
|
||||
certsDir = dir
|
||||
return
|
||||
}
|
||||
if os.Getenv("ROOTLESSKIT_STATE_DIR") != "" {
|
||||
// Configure registry.CertsDir() when running in rootless-mode
|
||||
// This is the equivalent of [rootless.RunningWithRootlessKit],
|
||||
// but inlining it to prevent adding that as a dependency
|
||||
// for docker/cli.
|
||||
//
|
||||
// [rootless.RunningWithRootlessKit]: https://github.com/moby/moby/blob/b4bdf12daec84caaf809a639f923f7370d4926ad/pkg/rootless/rootless.go#L5-L8
|
||||
if configHome, _ := homedir.GetConfigHome(); configHome != "" {
|
||||
certsDir = filepath.Join(configHome, "docker/certs.d")
|
||||
return
|
||||
}
|
||||
}
|
||||
certsDir = defaultCertsDir
|
||||
})
|
||||
return certsDir
|
||||
}
|
||||
|
||||
// SetCertsDir allows the default certs directory to be changed. This function
|
||||
// is used at daemon startup to set the correct location when running in
|
||||
// rootless mode.
|
||||
// runningWithRootlessKit is a fork of [rootless.RunningWithRootlessKit],
|
||||
// but inlining it to prevent adding that as a dependency for docker/cli.
|
||||
//
|
||||
// Deprecated: the cert-directory is now automatically selected when running with rootlessKit, and should no longer be set manually.
|
||||
func SetCertsDir(path string) {
|
||||
setCertsDir(path)
|
||||
// [rootless.RunningWithRootlessKit]: https://github.com/moby/moby/blob/b4bdf12daec84caaf809a639f923f7370d4926ad/pkg/rootless/rootless.go#L5-L8
|
||||
func runningWithRootlessKit() bool {
|
||||
return runtime.GOOS == "linux" && os.Getenv("ROOTLESSKIT_STATE_DIR") != ""
|
||||
}
|
||||
|
||||
// CertsDir is the directory where certificates are stored.
|
||||
//
|
||||
// - Linux: "/etc/docker/certs.d/"
|
||||
// - Linux (with rootlessKit): $XDG_CONFIG_HOME/docker/certs.d/" or "$HOME/.config/docker/certs.d/"
|
||||
// - Windows: "%PROGRAMDATA%/docker/certs.d/"
|
||||
//
|
||||
// TODO(thaJeztah): certsDir but stored in our config, and passed when needed. For the CLI, we should also default to same path as rootless.
|
||||
func CertsDir() string {
|
||||
// call setCertsDir with an empty path to synchronise with [SetCertsDir]
|
||||
return setCertsDir("")
|
||||
certsDir := "/etc/docker/certs.d"
|
||||
if runningWithRootlessKit() {
|
||||
if configHome, _ := os.UserConfigDir(); configHome != "" {
|
||||
certsDir = filepath.Join(configHome, "docker", "certs.d")
|
||||
}
|
||||
} else if runtime.GOOS == "windows" {
|
||||
certsDir = filepath.Join(os.Getenv("programdata"), "docker", "certs.d")
|
||||
}
|
||||
return certsDir
|
||||
}
|
||||
|
||||
// newServiceConfig returns a new instance of ServiceConfig
|
||||
|
|
@ -181,14 +168,15 @@ skip:
|
|||
if _, err := ValidateIndexName(r); err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.HasPrefix(strings.ToLower(r), "http://") {
|
||||
log.G(context.TODO()).Warnf("insecure registry %s should not contain 'http://' and 'http://' has been removed from the insecure registry config", r)
|
||||
r = r[7:]
|
||||
} else if strings.HasPrefix(strings.ToLower(r), "https://") {
|
||||
log.G(context.TODO()).Warnf("insecure registry %s should not contain 'https://' and 'https://' has been removed from the insecure registry config", r)
|
||||
r = r[8:]
|
||||
} else if hasScheme(r) {
|
||||
return invalidParamf("insecure registry %s should not contain '://'", r)
|
||||
if scheme, host, ok := strings.Cut(r, "://"); ok {
|
||||
switch strings.ToLower(scheme) {
|
||||
case "http", "https":
|
||||
log.G(context.TODO()).Warnf("insecure registry %[1]s should not contain '%[2]s' and '%[2]ss' has been removed from the insecure registry config", r, scheme)
|
||||
r = host
|
||||
default:
|
||||
// unsupported scheme
|
||||
return invalidParamf("insecure registry %s should not contain '://'", r)
|
||||
}
|
||||
}
|
||||
// Check if CIDR was passed to --insecure-registry
|
||||
_, ipnet, err := net.ParseCIDR(r)
|
||||
|
|
@ -253,18 +241,18 @@ func (config *serviceConfig) isSecureIndex(indexName string) bool {
|
|||
// for mocking in unit tests.
|
||||
var lookupIP = net.LookupIP
|
||||
|
||||
// isCIDRMatch returns true if URLHost matches an element of cidrs. URLHost is a URL.Host (`host:port` or `host`)
|
||||
// isCIDRMatch returns true if urlHost matches an element of cidrs. urlHost is 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 to IP addresses for matching. If resolution fails, false is returned.
|
||||
func isCIDRMatch(cidrs []*registry.NetIPNet, URLHost string) bool {
|
||||
func isCIDRMatch(cidrs []*registry.NetIPNet, urlHost string) bool {
|
||||
if len(cidrs) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
host, _, err := net.SplitHostPort(URLHost)
|
||||
host, _, err := net.SplitHostPort(urlHost)
|
||||
if err != nil {
|
||||
// Assume URLHost is a host without port and go on.
|
||||
host = URLHost
|
||||
// Assume urlHost is a host without port and go on.
|
||||
host = urlHost
|
||||
}
|
||||
|
||||
var addresses []net.IP
|
||||
|
|
@ -353,7 +341,7 @@ func validateHostPort(s string) error {
|
|||
}
|
||||
// If match against the `host:port` pattern fails,
|
||||
// it might be `IPv6:port`, which will be captured by net.ParseIP(host)
|
||||
if !validHostPortRegex.MatchString(s) && net.ParseIP(host) == nil {
|
||||
if !validHostPortRegex().MatchString(s) && net.ParseIP(host) == nil {
|
||||
return invalidParamf("invalid host %q", host)
|
||||
}
|
||||
if port != "" {
|
||||
|
|
@ -394,25 +382,6 @@ func GetAuthConfigKey(index *registry.IndexInfo) string {
|
|||
return index.Name
|
||||
}
|
||||
|
||||
// newRepositoryInfo validates and breaks down a repository name into a RepositoryInfo
|
||||
func newRepositoryInfo(config *serviceConfig, name reference.Named) *RepositoryInfo {
|
||||
index := newIndexInfo(config, reference.Domain(name))
|
||||
var officialRepo bool
|
||||
if index.Official {
|
||||
// RepositoryInfo.Official indicates whether the image repository
|
||||
// is an official (docker library official images) repository.
|
||||
//
|
||||
// We only need to check this if the image-repository is on Docker Hub.
|
||||
officialRepo = !strings.ContainsRune(reference.FamiliarName(name), '/')
|
||||
}
|
||||
|
||||
return &RepositoryInfo{
|
||||
Name: reference.TrimNamed(name),
|
||||
Index: index,
|
||||
Official: officialRepo,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseRepositoryInfo performs the breakdown of a repository name into a
|
||||
// [RepositoryInfo], but lacks registry configuration.
|
||||
//
|
||||
|
|
@ -428,7 +397,6 @@ func ParseRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) {
|
|||
Secure: true,
|
||||
Official: true,
|
||||
},
|
||||
Official: !strings.ContainsRune(reference.FamiliarName(reposName), '/'),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,340 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestValidateMirror(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
output string
|
||||
expectedErr string
|
||||
}{
|
||||
// Valid cases
|
||||
{
|
||||
input: "http://mirror-1.example.com",
|
||||
output: "http://mirror-1.example.com/",
|
||||
},
|
||||
{
|
||||
input: "http://mirror-1.example.com/",
|
||||
output: "http://mirror-1.example.com/",
|
||||
},
|
||||
{
|
||||
input: "https://mirror-1.example.com",
|
||||
output: "https://mirror-1.example.com/",
|
||||
},
|
||||
{
|
||||
input: "https://mirror-1.example.com/",
|
||||
output: "https://mirror-1.example.com/",
|
||||
},
|
||||
{
|
||||
input: "http://localhost",
|
||||
output: "http://localhost/",
|
||||
},
|
||||
{
|
||||
input: "https://localhost",
|
||||
output: "https://localhost/",
|
||||
},
|
||||
{
|
||||
input: "http://localhost:5000",
|
||||
output: "http://localhost:5000/",
|
||||
},
|
||||
{
|
||||
input: "https://localhost:5000",
|
||||
output: "https://localhost:5000/",
|
||||
},
|
||||
{
|
||||
input: "http://127.0.0.1",
|
||||
output: "http://127.0.0.1/",
|
||||
},
|
||||
{
|
||||
input: "https://127.0.0.1",
|
||||
output: "https://127.0.0.1/",
|
||||
},
|
||||
{
|
||||
input: "http://127.0.0.1:5000",
|
||||
output: "http://127.0.0.1:5000/",
|
||||
},
|
||||
{
|
||||
input: "https://127.0.0.1:5000",
|
||||
output: "https://127.0.0.1:5000/",
|
||||
},
|
||||
{
|
||||
input: "http://mirror-1.example.com/v1/",
|
||||
output: "http://mirror-1.example.com/v1/",
|
||||
},
|
||||
{
|
||||
input: "https://mirror-1.example.com/v1/",
|
||||
output: "https://mirror-1.example.com/v1/",
|
||||
},
|
||||
|
||||
// Invalid cases
|
||||
{
|
||||
input: "!invalid!://%as%",
|
||||
expectedErr: `invalid mirror: "!invalid!://%as%" is not a valid URI: parse "!invalid!://%as%": first path segment in URL cannot contain colon`,
|
||||
},
|
||||
{
|
||||
input: "mirror-1.example.com",
|
||||
expectedErr: `invalid mirror: no scheme specified for "mirror-1.example.com": must use either 'https://' or 'http://'`,
|
||||
},
|
||||
{
|
||||
input: "mirror-1.example.com:5000",
|
||||
expectedErr: `invalid mirror: no scheme specified for "mirror-1.example.com:5000": must use either 'https://' or 'http://'`,
|
||||
},
|
||||
{
|
||||
input: "ftp://mirror-1.example.com",
|
||||
expectedErr: `invalid mirror: unsupported scheme "ftp" in "ftp://mirror-1.example.com": must use either 'https://' or 'http://'`,
|
||||
},
|
||||
{
|
||||
input: "http://mirror-1.example.com/?q=foo",
|
||||
expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com/?q=foo"`,
|
||||
},
|
||||
{
|
||||
input: "http://mirror-1.example.com/v1/?q=foo",
|
||||
expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com/v1/?q=foo"`,
|
||||
},
|
||||
{
|
||||
input: "http://mirror-1.example.com/v1/?q=foo#frag",
|
||||
expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com/v1/?q=foo#frag"`,
|
||||
},
|
||||
{
|
||||
input: "http://mirror-1.example.com?q=foo",
|
||||
expectedErr: `invalid mirror: query or fragment at end of the URI "http://mirror-1.example.com?q=foo"`,
|
||||
},
|
||||
{
|
||||
input: "https://mirror-1.example.com#frag",
|
||||
expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com#frag"`,
|
||||
},
|
||||
{
|
||||
input: "https://mirror-1.example.com/#frag",
|
||||
expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com/#frag"`,
|
||||
},
|
||||
{
|
||||
input: "http://foo:bar@mirror-1.example.com/",
|
||||
expectedErr: `invalid mirror: username/password not allowed in URI "http://foo:xxxxx@mirror-1.example.com/"`,
|
||||
},
|
||||
{
|
||||
input: "https://mirror-1.example.com/v1/#frag",
|
||||
expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com/v1/#frag"`,
|
||||
},
|
||||
{
|
||||
input: "https://mirror-1.example.com?q",
|
||||
expectedErr: `invalid mirror: query or fragment at end of the URI "https://mirror-1.example.com?q"`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
out, err := ValidateMirror(tc.input)
|
||||
if tc.expectedErr != "" {
|
||||
assert.Error(t, err, tc.expectedErr)
|
||||
} else {
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
assert.Check(t, is.Equal(out, tc.output))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadInsecureRegistries(t *testing.T) {
|
||||
testCases := []struct {
|
||||
registries []string
|
||||
index string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
registries: []string{"127.0.0.1"},
|
||||
index: "127.0.0.1",
|
||||
},
|
||||
{
|
||||
registries: []string{"127.0.0.1:8080"},
|
||||
index: "127.0.0.1:8080",
|
||||
},
|
||||
{
|
||||
registries: []string{"2001:db8::1"},
|
||||
index: "2001:db8::1",
|
||||
},
|
||||
{
|
||||
registries: []string{"[2001:db8::1]:80"},
|
||||
index: "[2001:db8::1]:80",
|
||||
},
|
||||
{
|
||||
registries: []string{"http://myregistry.example.com"},
|
||||
index: "myregistry.example.com",
|
||||
},
|
||||
{
|
||||
registries: []string{"https://myregistry.example.com"},
|
||||
index: "myregistry.example.com",
|
||||
},
|
||||
{
|
||||
registries: []string{"HTTP://myregistry.example.com"},
|
||||
index: "myregistry.example.com",
|
||||
},
|
||||
{
|
||||
registries: []string{"svn://myregistry.example.com"},
|
||||
err: "insecure registry svn://myregistry.example.com should not contain '://'",
|
||||
},
|
||||
{
|
||||
registries: []string{"-invalid-registry"},
|
||||
err: "Cannot begin or end with a hyphen",
|
||||
},
|
||||
{
|
||||
registries: []string{`mytest-.com`},
|
||||
err: `insecure registry mytest-.com is not valid: invalid host "mytest-.com"`,
|
||||
},
|
||||
{
|
||||
registries: []string{`1200:0000:AB00:1234:0000:2552:7777:1313:8080`},
|
||||
err: `insecure registry 1200:0000:AB00:1234:0000:2552:7777:1313:8080 is not valid: invalid host "1200:0000:AB00:1234:0000:2552:7777:1313:8080"`,
|
||||
},
|
||||
{
|
||||
registries: []string{`myregistry.example.com:500000`},
|
||||
err: `insecure registry myregistry.example.com:500000 is not valid: invalid port "500000"`,
|
||||
},
|
||||
{
|
||||
registries: []string{`"myregistry.example.com"`},
|
||||
err: `insecure registry "myregistry.example.com" is not valid: invalid host "\"myregistry.example.com\""`,
|
||||
},
|
||||
{
|
||||
registries: []string{`"myregistry.example.com:5000"`},
|
||||
err: `insecure registry "myregistry.example.com:5000" is not valid: invalid host "\"myregistry.example.com"`,
|
||||
},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
config := &serviceConfig{}
|
||||
err := config.loadInsecureRegistries(testCase.registries)
|
||||
if testCase.err == "" {
|
||||
if err != nil {
|
||||
t.Fatalf("expect no error, got '%s'", err)
|
||||
}
|
||||
match := false
|
||||
for index := range config.IndexConfigs {
|
||||
if index == testCase.index {
|
||||
match = true
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
t.Fatalf("expect index configs to contain '%s', got %+v", testCase.index, config.IndexConfigs)
|
||||
}
|
||||
} else {
|
||||
if err == nil {
|
||||
t.Fatalf("expect error '%s', got no error", testCase.err)
|
||||
}
|
||||
assert.ErrorContains(t, err, testCase.err)
|
||||
assert.Check(t, cerrdefs.IsInvalidArgument(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewServiceConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
doc string
|
||||
opts ServiceOptions
|
||||
errStr string
|
||||
}{
|
||||
{
|
||||
doc: "empty config",
|
||||
},
|
||||
{
|
||||
doc: "invalid mirror",
|
||||
opts: ServiceOptions{
|
||||
Mirrors: []string{"example.com:5000"},
|
||||
},
|
||||
errStr: `invalid mirror: no scheme specified for "example.com:5000": must use either 'https://' or 'http://'`,
|
||||
},
|
||||
{
|
||||
doc: "valid mirror",
|
||||
opts: ServiceOptions{
|
||||
Mirrors: []string{"https://example.com:5000"},
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: "invalid insecure registry",
|
||||
opts: ServiceOptions{
|
||||
InsecureRegistries: []string{"[fe80::]/64"},
|
||||
},
|
||||
errStr: `insecure registry [fe80::]/64 is not valid: invalid host "[fe80::]/64"`,
|
||||
},
|
||||
{
|
||||
doc: "valid insecure registry",
|
||||
opts: ServiceOptions{
|
||||
InsecureRegistries: []string{"102.10.8.1/24"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
_, err := newServiceConfig(tc.opts)
|
||||
if tc.errStr != "" {
|
||||
assert.Check(t, is.Error(err, tc.errStr))
|
||||
assert.Check(t, cerrdefs.IsInvalidArgument(err))
|
||||
} else {
|
||||
assert.Check(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateIndexName(t *testing.T) {
|
||||
valid := []struct {
|
||||
index string
|
||||
expect string
|
||||
}{
|
||||
{
|
||||
index: "index.docker.io",
|
||||
expect: "docker.io",
|
||||
},
|
||||
{
|
||||
index: "example.com",
|
||||
expect: "example.com",
|
||||
},
|
||||
{
|
||||
index: "127.0.0.1:8080",
|
||||
expect: "127.0.0.1:8080",
|
||||
},
|
||||
{
|
||||
index: "mytest-1.com",
|
||||
expect: "mytest-1.com",
|
||||
},
|
||||
{
|
||||
index: "mirror-1.example.com/v1/?q=foo",
|
||||
expect: "mirror-1.example.com/v1/?q=foo",
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range valid {
|
||||
result, err := ValidateIndexName(testCase.index)
|
||||
if assert.Check(t, err) {
|
||||
assert.Check(t, is.Equal(testCase.expect, result))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateIndexNameWithError(t *testing.T) {
|
||||
invalid := []struct {
|
||||
index string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
index: "docker.io-",
|
||||
err: "invalid index name (docker.io-). Cannot begin or end with a hyphen",
|
||||
},
|
||||
{
|
||||
index: "-example.com",
|
||||
err: "invalid index name (-example.com). Cannot begin or end with a hyphen",
|
||||
},
|
||||
{
|
||||
index: "mirror-1.example.com/v1/?q=foo-",
|
||||
err: "invalid index name (mirror-1.example.com/v1/?q=foo-). Cannot begin or end with a hyphen",
|
||||
},
|
||||
}
|
||||
for _, testCase := range invalid {
|
||||
_, err := ValidateIndexName(testCase.index)
|
||||
assert.Check(t, is.Error(err, testCase.err))
|
||||
assert.Check(t, cerrdefs.IsInvalidArgument(err))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
// Package registry is a fork of [github.com/docker/docker/registry], taken
|
||||
// at commit [moby@49306c6]. Git history was not preserved in this fork,
|
||||
// but can be found using the URLs provided.
|
||||
//
|
||||
// This fork was created to remove the dependency on the "Moby" codebase,
|
||||
// and because the CLI only needs a subset of its features. The original
|
||||
// package was written specifically for use in the daemon code, and includes
|
||||
// functionality that cannot be used in the CLI.
|
||||
//
|
||||
// [github.com/docker/docker/registry]: https://pkg.go.dev/github.com/docker/docker@v28.3.2+incompatible/registry
|
||||
// [moby@49306c6]: https://github.com/moby/moby/tree/49306c607b72c5bf0a8e426f5a9760fa5ef96ea0/registry
|
||||
package registry
|
||||
|
|
@ -8,17 +8,13 @@ import (
|
|||
)
|
||||
|
||||
func translateV2AuthError(err error) error {
|
||||
switch e := err.(type) {
|
||||
case *url.Error:
|
||||
switch e2 := e.Err.(type) {
|
||||
case errcode.Error:
|
||||
switch e2.Code {
|
||||
case errcode.ErrorCodeUnauthorized:
|
||||
return unauthorizedErr{err}
|
||||
}
|
||||
var e *url.Error
|
||||
if errors.As(err, &e) {
|
||||
var e2 errcode.Error
|
||||
if errors.As(e, &e2) && errors.Is(e2.Code, errcode.ErrorCodeUnauthorized) {
|
||||
return unauthorizedErr{err}
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -8,6 +8,8 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/log"
|
||||
|
|
@ -16,16 +18,15 @@ import (
|
|||
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
|
||||
)
|
||||
|
||||
// HostCertsDir returns the config directory for a specific host.
|
||||
//
|
||||
// Deprecated: this function was only used internally, and will be removed in a future release.
|
||||
func HostCertsDir(hostname string) string {
|
||||
return hostCertsDir(hostname)
|
||||
}
|
||||
|
||||
// hostCertsDir returns the config directory for a specific host.
|
||||
func hostCertsDir(hostname string) string {
|
||||
return filepath.Join(CertsDir(), cleanPath(hostname))
|
||||
func hostCertsDir(hostnameAndPort string) string {
|
||||
if runtime.GOOS == "windows" {
|
||||
// Ensure that a directory name is valid; hostnameAndPort may contain
|
||||
// a colon (:) if a port is included, and Windows does not allow colons
|
||||
// in directory names.
|
||||
hostnameAndPort = filepath.FromSlash(strings.ReplaceAll(hostnameAndPort, ":", ""))
|
||||
}
|
||||
return filepath.Join(CertsDir(), hostnameAndPort)
|
||||
}
|
||||
|
||||
// newTLSConfig constructs a client TLS configuration based on server defaults
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/log"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
testHTTPServer *httptest.Server
|
||||
testHTTPSServer *httptest.Server
|
||||
)
|
||||
|
||||
func init() {
|
||||
r := http.NewServeMux()
|
||||
|
||||
// /v1/
|
||||
r.HandleFunc("/v1/_ping", handlerGetPing)
|
||||
r.HandleFunc("/v1/search", handlerSearch)
|
||||
|
||||
// /v2/
|
||||
r.HandleFunc("/v2/version", handlerGetPing)
|
||||
|
||||
testHTTPServer = httptest.NewServer(handlerAccessLog(r))
|
||||
testHTTPSServer = httptest.NewTLSServer(handlerAccessLog(r))
|
||||
}
|
||||
|
||||
func handlerAccessLog(handler http.Handler) http.Handler {
|
||||
logHandler := func(w http.ResponseWriter, r *http.Request) {
|
||||
log.G(context.TODO()).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) *registry.IndexInfo {
|
||||
return ®istry.IndexInfo{
|
||||
Name: makeURL(req),
|
||||
}
|
||||
}
|
||||
|
||||
func makeHTTPSIndex(req string) *registry.IndexInfo {
|
||||
return ®istry.IndexInfo{
|
||||
Name: makeHTTPSURL(req),
|
||||
}
|
||||
}
|
||||
|
||||
func makePublicIndex() *registry.IndexInfo {
|
||||
return ®istry.IndexInfo{
|
||||
Name: IndexServer,
|
||||
Secure: true,
|
||||
Official: true,
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
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 handlerGetPing(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeResponse(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
writeResponse(w, true, http.StatusOK)
|
||||
}
|
||||
|
||||
func handlerSearch(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeResponse(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
result := ®istry.SearchResults{
|
||||
Query: "fakequery",
|
||||
NumResults: 1,
|
||||
Results: []registry.SearchResult{{Name: "fakeimage", StarCount: 42}},
|
||||
}
|
||||
writeResponse(w, result, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestPing(t *testing.T) {
|
||||
res, err := http.Get(makeURL("/v1/_ping"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, res.StatusCode, http.StatusOK, "")
|
||||
assert.Equal(t, res.Header.Get("Server"), "docker-tests/mock")
|
||||
_ = res.Body.Close()
|
||||
}
|
||||
|
|
@ -0,0 +1,637 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
// overrideLookupIP overrides net.LookupIP for testing.
|
||||
func overrideLookupIP(t *testing.T) {
|
||||
t.Helper()
|
||||
restoreLookup := lookupIP
|
||||
|
||||
// override net.LookupIP
|
||||
lookupIP = func(host string) ([]net.IP, error) {
|
||||
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")},
|
||||
}
|
||||
if addrs, ok := mockHosts[host]; ok {
|
||||
return addrs, nil
|
||||
}
|
||||
return nil, errors.New("lookup: no such host")
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
lookupIP = restoreLookup
|
||||
})
|
||||
}
|
||||
|
||||
func TestParseRepositoryInfo(t *testing.T) {
|
||||
type staticRepositoryInfo struct {
|
||||
Index *registry.IndexInfo
|
||||
RemoteName string
|
||||
CanonicalName string
|
||||
LocalName string
|
||||
}
|
||||
|
||||
tests := map[string]staticRepositoryInfo{
|
||||
"fooo/bar": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Mirrors: []string{},
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "fooo/bar",
|
||||
LocalName: "fooo/bar",
|
||||
CanonicalName: "docker.io/fooo/bar",
|
||||
},
|
||||
"library/ubuntu": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Mirrors: []string{},
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu",
|
||||
LocalName: "ubuntu",
|
||||
CanonicalName: "docker.io/library/ubuntu",
|
||||
},
|
||||
"nonlibrary/ubuntu": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Mirrors: []string{},
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "nonlibrary/ubuntu",
|
||||
LocalName: "nonlibrary/ubuntu",
|
||||
CanonicalName: "docker.io/nonlibrary/ubuntu",
|
||||
},
|
||||
"ubuntu": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Mirrors: []string{},
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu",
|
||||
LocalName: "ubuntu",
|
||||
CanonicalName: "docker.io/library/ubuntu",
|
||||
},
|
||||
"other/library": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Mirrors: []string{},
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "other/library",
|
||||
LocalName: "other/library",
|
||||
CanonicalName: "docker.io/other/library",
|
||||
},
|
||||
"127.0.0.1:8000/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "127.0.0.1:8000",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "127.0.0.1:8000/private/moonbase",
|
||||
CanonicalName: "127.0.0.1:8000/private/moonbase",
|
||||
},
|
||||
"127.0.0.1:8000/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "127.0.0.1:8000",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "127.0.0.1:8000/privatebase",
|
||||
CanonicalName: "127.0.0.1:8000/privatebase",
|
||||
},
|
||||
"[::1]:8000/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "[::1]:8000",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "[::1]:8000/private/moonbase",
|
||||
CanonicalName: "[::1]:8000/private/moonbase",
|
||||
},
|
||||
"[::1]:8000/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "[::1]:8000",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "[::1]:8000/privatebase",
|
||||
CanonicalName: "[::1]:8000/privatebase",
|
||||
},
|
||||
// IPv6 only has a single loopback address, so ::2 is not a loopback,
|
||||
// hence not marked "insecure".
|
||||
"[::2]:8000/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "[::2]:8000",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "[::2]:8000/private/moonbase",
|
||||
CanonicalName: "[::2]:8000/private/moonbase",
|
||||
},
|
||||
// IPv6 only has a single loopback address, so ::2 is not a loopback,
|
||||
// hence not marked "insecure".
|
||||
"[::2]:8000/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "[::2]:8000",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "[::2]:8000/privatebase",
|
||||
CanonicalName: "[::2]:8000/privatebase",
|
||||
},
|
||||
"localhost:8000/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "localhost:8000",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "localhost:8000/private/moonbase",
|
||||
CanonicalName: "localhost:8000/private/moonbase",
|
||||
},
|
||||
"localhost:8000/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "localhost:8000",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "localhost:8000/privatebase",
|
||||
CanonicalName: "localhost:8000/privatebase",
|
||||
},
|
||||
"example.com/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "example.com",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "example.com/private/moonbase",
|
||||
CanonicalName: "example.com/private/moonbase",
|
||||
},
|
||||
"example.com/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "example.com",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "example.com/privatebase",
|
||||
CanonicalName: "example.com/privatebase",
|
||||
},
|
||||
"example.com:8000/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "example.com:8000",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "example.com:8000/private/moonbase",
|
||||
CanonicalName: "example.com:8000/private/moonbase",
|
||||
},
|
||||
"example.com:8000/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "example.com:8000",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "example.com:8000/privatebase",
|
||||
CanonicalName: "example.com:8000/privatebase",
|
||||
},
|
||||
"localhost/private/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "localhost",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "private/moonbase",
|
||||
LocalName: "localhost/private/moonbase",
|
||||
CanonicalName: "localhost/private/moonbase",
|
||||
},
|
||||
"localhost/privatebase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: "localhost",
|
||||
Mirrors: []string{},
|
||||
Official: false,
|
||||
Secure: false,
|
||||
},
|
||||
RemoteName: "privatebase",
|
||||
LocalName: "localhost/privatebase",
|
||||
CanonicalName: "localhost/privatebase",
|
||||
},
|
||||
IndexName + "/public/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Mirrors: []string{},
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "public/moonbase",
|
||||
LocalName: "public/moonbase",
|
||||
CanonicalName: "docker.io/public/moonbase",
|
||||
},
|
||||
"index." + IndexName + "/public/moonbase": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Mirrors: []string{},
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "public/moonbase",
|
||||
LocalName: "public/moonbase",
|
||||
CanonicalName: "docker.io/public/moonbase",
|
||||
},
|
||||
"ubuntu-12.04-base": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Mirrors: []string{},
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu-12.04-base",
|
||||
LocalName: "ubuntu-12.04-base",
|
||||
CanonicalName: "docker.io/library/ubuntu-12.04-base",
|
||||
},
|
||||
IndexName + "/ubuntu-12.04-base": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Mirrors: []string{},
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu-12.04-base",
|
||||
LocalName: "ubuntu-12.04-base",
|
||||
CanonicalName: "docker.io/library/ubuntu-12.04-base",
|
||||
},
|
||||
"index." + IndexName + "/ubuntu-12.04-base": {
|
||||
Index: ®istry.IndexInfo{
|
||||
Name: IndexName,
|
||||
Mirrors: []string{},
|
||||
Official: true,
|
||||
Secure: true,
|
||||
},
|
||||
RemoteName: "library/ubuntu-12.04-base",
|
||||
LocalName: "ubuntu-12.04-base",
|
||||
CanonicalName: "docker.io/library/ubuntu-12.04-base",
|
||||
},
|
||||
}
|
||||
|
||||
for reposName, expected := range tests {
|
||||
t.Run(reposName, func(t *testing.T) {
|
||||
named, err := reference.ParseNormalizedNamed(reposName)
|
||||
assert.NilError(t, err)
|
||||
|
||||
repoInfo, err := ParseRepositoryInfo(named)
|
||||
assert.NilError(t, err)
|
||||
|
||||
assert.Check(t, is.DeepEqual(repoInfo.Index, expected.Index))
|
||||
assert.Check(t, is.Equal(reference.Path(repoInfo.Name), expected.RemoteName))
|
||||
assert.Check(t, is.Equal(reference.FamiliarName(repoInfo.Name), expected.LocalName))
|
||||
assert.Check(t, is.Equal(repoInfo.Name.Name(), expected.CanonicalName))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewIndexInfo(t *testing.T) {
|
||||
overrideLookupIP(t)
|
||||
|
||||
// ipv6Loopback is the CIDR for the IPv6 loopback address ("::1"); "::1/128"
|
||||
ipv6Loopback := &net.IPNet{
|
||||
IP: net.IPv6loopback,
|
||||
Mask: net.CIDRMask(128, 128),
|
||||
}
|
||||
|
||||
// ipv4Loopback is the CIDR for IPv4 loopback addresses ("127.0.0.0/8")
|
||||
ipv4Loopback := &net.IPNet{
|
||||
IP: net.IPv4(127, 0, 0, 0),
|
||||
Mask: net.CIDRMask(8, 32),
|
||||
}
|
||||
|
||||
// emptyServiceConfig is a default service-config for situations where
|
||||
// no config-file is available (e.g. when used in the CLI). It won't
|
||||
// have mirrors configured, but does have the default insecure registry
|
||||
// CIDRs for loopback interfaces configured.
|
||||
emptyServiceConfig := &serviceConfig{
|
||||
IndexConfigs: map[string]*registry.IndexInfo{
|
||||
IndexName: {
|
||||
Name: IndexName,
|
||||
Mirrors: []string{},
|
||||
Secure: true,
|
||||
Official: true,
|
||||
},
|
||||
},
|
||||
InsecureRegistryCIDRs: []*registry.NetIPNet{
|
||||
(*registry.NetIPNet)(ipv6Loopback),
|
||||
(*registry.NetIPNet)(ipv4Loopback),
|
||||
},
|
||||
}
|
||||
|
||||
expectedIndexInfos := map[string]*registry.IndexInfo{
|
||||
IndexName: {
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"index." + IndexName: {
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"example.com": {
|
||||
Name: "example.com",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"127.0.0.1:5000": {
|
||||
Name: "127.0.0.1:5000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
}
|
||||
t.Run("no mirrors", func(t *testing.T) {
|
||||
for indexName, expected := range expectedIndexInfos {
|
||||
t.Run(indexName, func(t *testing.T) {
|
||||
actual := newIndexInfo(emptyServiceConfig, indexName)
|
||||
assert.Check(t, is.DeepEqual(actual, expected))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
expectedIndexInfos = map[string]*registry.IndexInfo{
|
||||
IndexName: {
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
Mirrors: []string{"http://mirror1.local/", "http://mirror2.local/"},
|
||||
},
|
||||
"index." + IndexName: {
|
||||
Name: IndexName,
|
||||
Official: true,
|
||||
Secure: true,
|
||||
Mirrors: []string{"http://mirror1.local/", "http://mirror2.local/"},
|
||||
},
|
||||
"example.com": {
|
||||
Name: "example.com",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"example.com:5000": {
|
||||
Name: "example.com:5000",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"127.0.0.1": {
|
||||
Name: "127.0.0.1",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"127.0.0.1:5000": {
|
||||
Name: "127.0.0.1:5000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"127.255.255.255": {
|
||||
Name: "127.255.255.255",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"127.255.255.255:5000": {
|
||||
Name: "127.255.255.255:5000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"::1": {
|
||||
Name: "::1",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"[::1]:5000": {
|
||||
Name: "[::1]:5000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
// IPv6 only has a single loopback address, so ::2 is not a loopback,
|
||||
// hence not marked "insecure".
|
||||
"::2": {
|
||||
Name: "::2",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
// IPv6 only has a single loopback address, so ::2 is not a loopback,
|
||||
// hence not marked "insecure".
|
||||
"[::2]:5000": {
|
||||
Name: "[::2]:5000",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"other.com": {
|
||||
Name: "other.com",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
}
|
||||
t.Run("mirrors", func(t *testing.T) {
|
||||
// Note that newServiceConfig calls ValidateMirror internally, which normalizes
|
||||
// mirror-URLs to have a trailing slash.
|
||||
config, err := newServiceConfig(ServiceOptions{
|
||||
Mirrors: []string{"http://mirror1.local", "http://mirror2.local"},
|
||||
InsecureRegistries: []string{"example.com"},
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
for indexName, expected := range expectedIndexInfos {
|
||||
t.Run(indexName, func(t *testing.T) {
|
||||
actual := newIndexInfo(config, indexName)
|
||||
assert.Check(t, is.DeepEqual(actual, expected))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
expectedIndexInfos = map[string]*registry.IndexInfo{
|
||||
"example.com": {
|
||||
Name: "example.com",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"example.com:5000": {
|
||||
Name: "example.com:5000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"127.0.0.1": {
|
||||
Name: "127.0.0.1",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"127.0.0.1:5000": {
|
||||
Name: "127.0.0.1:5000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"42.42.0.1:5000": {
|
||||
Name: "42.42.0.1:5000",
|
||||
Official: false,
|
||||
Secure: false,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"42.43.0.1:5000": {
|
||||
Name: "42.43.0.1:5000",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
"other.com": {
|
||||
Name: "other.com",
|
||||
Official: false,
|
||||
Secure: true,
|
||||
Mirrors: []string{},
|
||||
},
|
||||
}
|
||||
t.Run("custom insecure", func(t *testing.T) {
|
||||
config, err := newServiceConfig(ServiceOptions{
|
||||
InsecureRegistries: []string{"42.42.0.0/16"},
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
for indexName, expected := range expectedIndexInfos {
|
||||
t.Run(indexName, func(t *testing.T) {
|
||||
actual := newIndexInfo(config, indexName)
|
||||
assert.Check(t, is.DeepEqual(actual, expected))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMirrorEndpointLookup(t *testing.T) {
|
||||
containsMirror := func(endpoints []APIEndpoint) bool {
|
||||
for _, pe := range endpoints {
|
||||
if pe.URL.Host == "my.mirror" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
cfg, err := newServiceConfig(ServiceOptions{
|
||||
Mirrors: []string{"https://my.mirror"},
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
s := Service{config: cfg}
|
||||
|
||||
imageName, err := reference.WithName(IndexName + "/test/image")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
pushAPIEndpoints, err := s.LookupPushEndpoints(reference.Domain(imageName))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if containsMirror(pushAPIEndpoints) {
|
||||
t.Fatal("Push endpoint should not contain mirror")
|
||||
}
|
||||
|
||||
pullAPIEndpoints, err := s.LookupPullEndpoints(reference.Domain(imageName))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !containsMirror(pullAPIEndpoints) {
|
||||
t.Fatal("Pull endpoint should contain mirror")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsSecureIndex(t *testing.T) {
|
||||
overrideLookupIP(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.example.com", []string{"42.42.0.0/16"}, true},
|
||||
{"invalid.example.com", []string{"invalid.example.com"}, false},
|
||||
{"invalid.example.com:5000", []string{"invalid.example.com"}, true},
|
||||
{"invalid.example.com:5000", []string{"invalid.example.com:5000"}, false},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
config, err := newServiceConfig(ServiceOptions{
|
||||
InsecureRegistries: tc.insecureRegistries,
|
||||
})
|
||||
assert.NilError(t, err)
|
||||
|
||||
sec := config.isSecureIndex(tc.addr)
|
||||
assert.Equal(t, sec, tc.expected, "isSecureIndex failed for %q %v, expected %v got %v", tc.addr, tc.insecureRegistries, tc.expected, sec)
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
|
@ -58,7 +59,12 @@ func newV1Endpoint(ctx context.Context, index *registry.IndexInfo, headers http.
|
|||
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 fall back to HTTP.
|
||||
return nil, invalidParamf("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)
|
||||
hint := fmt.Sprintf(
|
||||
". If this private registry supports only HTTP or HTTPS with an unknown CA certificate, add `--insecure-registry %[1]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; place the CA certificate at /etc/docker/certs.d/%[1]s/ca.crt",
|
||||
endpoint.URL.Host,
|
||||
)
|
||||
return nil, invalidParamf("invalid registry endpoint %s: %v%s", endpoint, err, hint)
|
||||
}
|
||||
|
||||
// registry is insecure and HTTPS failed, fallback to HTTP.
|
||||
|
|
@ -163,9 +169,9 @@ func (e *v1Endpoint) ping(ctx context.Context) (v1PingResult, error) {
|
|||
|
||||
// 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 {
|
||||
func httpClient(tr http.RoundTripper) *http.Client {
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
Transport: tr,
|
||||
CheckRedirect: addRequiredHeadersToRedirectedRequests,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
func TestV1EndpointPing(t *testing.T) {
|
||||
testPing := func(index *registry.IndexInfo, expectedStandalone bool, assertMessage string) {
|
||||
ep, err := newV1Endpoint(context.Background(), index, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
regInfo, err := ep.ping(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(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 TestV1Endpoint(t *testing.T) {
|
||||
// Simple wrapper to fail test if err != nil
|
||||
expandEndpoint := func(index *registry.IndexInfo) *v1Endpoint {
|
||||
endpoint, err := newV1Endpoint(context.Background(), index, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return endpoint
|
||||
}
|
||||
|
||||
assertInsecureIndex := func(index *registry.IndexInfo) {
|
||||
index.Secure = true
|
||||
_, err := newV1Endpoint(context.Background(), index, nil)
|
||||
assert.ErrorContains(t, err, "insecure-registry", index.Name+": Expected insecure-registry error for insecure index")
|
||||
index.Secure = false
|
||||
}
|
||||
|
||||
assertSecureIndex := func(index *registry.IndexInfo) {
|
||||
index.Secure = true
|
||||
_, err := newV1Endpoint(context.Background(), index, nil)
|
||||
assert.ErrorContains(t, err, "certificate signed by unknown authority", index.Name+": Expected cert error for secure index")
|
||||
index.Secure = false
|
||||
}
|
||||
|
||||
index := ®istry.IndexInfo{}
|
||||
index.Name = makeURL("/v1/")
|
||||
endpoint := expandEndpoint(index)
|
||||
assert.Equal(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name)
|
||||
assertInsecureIndex(index)
|
||||
|
||||
index.Name = makeURL("")
|
||||
endpoint = expandEndpoint(index)
|
||||
assert.Equal(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)
|
||||
assert.Equal(t, endpoint.String(), httpURL+"/v1/", index.Name+": Expected endpoint to be "+httpURL+"/v1/")
|
||||
assertInsecureIndex(index)
|
||||
|
||||
index.Name = makeHTTPSURL("/v1/")
|
||||
endpoint = expandEndpoint(index)
|
||||
assert.Equal(t, endpoint.String(), index.Name, "Expected endpoint to be "+index.Name)
|
||||
assertSecureIndex(index)
|
||||
|
||||
index.Name = makeHTTPSURL("")
|
||||
endpoint = expandEndpoint(index)
|
||||
assert.Equal(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)
|
||||
assert.Equal(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(context.Background(), index, nil)
|
||||
assert.Check(t, err != nil, "Expected error while expanding bad endpoint: %s", address)
|
||||
}
|
||||
}
|
||||
|
||||
func TestV1EndpointParse(t *testing.T) {
|
||||
tests := []struct {
|
||||
address string
|
||||
expected string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
address: IndexServer,
|
||||
expected: IndexServer,
|
||||
},
|
||||
{
|
||||
address: "https://0.0.0.0:5000/v1/",
|
||||
expected: "https://0.0.0.0:5000/v1/",
|
||||
},
|
||||
{
|
||||
address: "https://0.0.0.0:5000",
|
||||
expected: "https://0.0.0.0:5000/v1/",
|
||||
},
|
||||
{
|
||||
address: "0.0.0.0:5000",
|
||||
expected: "https://0.0.0.0:5000/v1/",
|
||||
},
|
||||
{
|
||||
address: "https://0.0.0.0:5000/nonversion/",
|
||||
expected: "https://0.0.0.0:5000/nonversion/v1/",
|
||||
},
|
||||
{
|
||||
address: "https://0.0.0.0:5000/v0/",
|
||||
expected: "https://0.0.0.0:5000/v0/v1/",
|
||||
},
|
||||
{
|
||||
address: "https://0.0.0.0:5000/v2/",
|
||||
expectedErr: "search is not supported on v2 endpoints: https://0.0.0.0:5000/v2/",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.address, func(t *testing.T) {
|
||||
ep, err := newV1EndpointFromStr(tc.address, nil, nil)
|
||||
if tc.expectedErr != "" {
|
||||
assert.Check(t, is.Error(err, tc.expectedErr))
|
||||
assert.Check(t, is.Nil(ep))
|
||||
} else {
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal(ep.String(), tc.expected))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that a registry endpoint that responds with a 401 only is determined
|
||||
// to be a valid v1 registry endpoint
|
||||
func TestV1EndpointValidate(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()
|
||||
|
||||
testEndpoint, err := newV1Endpoint(context.Background(), ®istry.IndexInfo{Name: testServer.URL}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if testEndpoint.URL.Scheme != "http" {
|
||||
t.Fatalf("expecting to validate endpoint as http, got url %s", testEndpoint.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrustedLocation(t *testing.T) {
|
||||
for _, u := range []string{"http://example.com", "https://example.com:7777", "http://docker.io", "http://test.docker.com", "https://fakedocker.com"} {
|
||||
req, _ := http.NewRequest(http.MethodGet, u, http.NoBody)
|
||||
assert.Check(t, !trustedLocation(req))
|
||||
}
|
||||
|
||||
for _, u := range []string{"https://docker.io", "https://test.docker.com:80"} {
|
||||
req, _ := http.NewRequest(http.MethodGet, u, http.NoBody)
|
||||
assert.Check(t, trustedLocation(req))
|
||||
}
|
||||
}
|
||||
|
||||
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(http.MethodGet, urls[0], http.NoBody)
|
||||
reqFrom.Header.Add("Content-Type", "application/json")
|
||||
reqFrom.Header.Add("Authorization", "super_secret")
|
||||
reqTo, _ := http.NewRequest(http.MethodGet, urls[1], http.NoBody)
|
||||
|
||||
_ = 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(http.MethodGet, urls[0], http.NoBody)
|
||||
reqFrom.Header.Add("Content-Type", "application/json")
|
||||
reqFrom.Header.Add("Authorization", "super_secret")
|
||||
reqTo, _ := http.NewRequest(http.MethodGet, urls[1], http.NoBody)
|
||||
|
||||
_ = 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'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
|
|
@ -219,7 +220,7 @@ func (r *session) searchRepositories(ctx context.Context, term string, limit int
|
|||
if limit < 1 || limit > 100 {
|
||||
return nil, invalidParamf("limit %d is outside the range of [1, 100]", limit)
|
||||
}
|
||||
u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term) + "&n=" + url.QueryEscape(fmt.Sprintf("%d", limit))
|
||||
u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term) + "&n=" + url.QueryEscape(strconv.Itoa(limit))
|
||||
log.G(ctx).WithField("url", u).Debug("searchRepositories")
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, http.NoBody)
|
||||
|
|
@ -236,7 +237,7 @@ func (r *session) searchRepositories(ctx context.Context, term string, limit int
|
|||
if res.StatusCode != http.StatusOK {
|
||||
// TODO(thaJeztah): return upstream response body for errors (see https://github.com/moby/moby/issues/27286).
|
||||
// TODO(thaJeztah): handle other status-codes to return correct error-type
|
||||
return nil, errUnknown{fmt.Errorf("Unexpected status code %d", res.StatusCode)}
|
||||
return nil, errUnknown{fmt.Errorf("unexpected status code %d", res.StatusCode)}
|
||||
}
|
||||
result := ®istry.SearchResults{}
|
||||
err = json.NewDecoder(res.Body).Decode(result)
|
||||
|
|
@ -0,0 +1,418 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/http/httputil"
|
||||
"testing"
|
||||
|
||||
cerrdefs "github.com/containerd/errdefs"
|
||||
"github.com/docker/distribution/registry/client/transport"
|
||||
"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func spawnTestRegistrySession(t *testing.T) *session {
|
||||
t.Helper()
|
||||
authConfig := ®istry.AuthConfig{}
|
||||
endpoint, err := newV1Endpoint(context.Background(), 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(newAuthTransport(tr, authConfig, false), Headers(userAgent, nil)...)
|
||||
client := httpClient(tr)
|
||||
|
||||
if err := authorizeClient(context.Background(), client, authConfig, endpoint); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := newSession(client, endpoint)
|
||||
|
||||
// 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 = []string{"fake-token"}
|
||||
return r
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func TestSearchRepositories(t *testing.T) {
|
||||
r := spawnTestRegistrySession(t)
|
||||
results, err := r.searchRepositories(context.Background(), "fakequery", 25)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if results == nil {
|
||||
t.Fatal("Expected non-nil SearchResults object")
|
||||
}
|
||||
assert.Equal(t, results.NumResults, 1, "Expected 1 search results")
|
||||
assert.Equal(t, results.Query, "fakequery", "Expected 'fakequery' as query")
|
||||
assert.Equal(t, results.Results[0].StarCount, 42, "Expected 'fakeimage' to have 42 stars")
|
||||
}
|
||||
|
||||
func TestSearchErrors(t *testing.T) {
|
||||
errorCases := []struct {
|
||||
filtersArgs filters.Args
|
||||
shouldReturnError bool
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
expectedError: "unexpected status code 500",
|
||||
shouldReturnError: true,
|
||||
},
|
||||
{
|
||||
filtersArgs: filters.NewArgs(filters.Arg("type", "custom")),
|
||||
expectedError: "invalid filter 'type'",
|
||||
},
|
||||
{
|
||||
filtersArgs: filters.NewArgs(filters.Arg("is-automated", "invalid")),
|
||||
expectedError: "invalid filter 'is-automated=[invalid]'",
|
||||
},
|
||||
{
|
||||
filtersArgs: filters.NewArgs(
|
||||
filters.Arg("is-automated", "true"),
|
||||
filters.Arg("is-automated", "false"),
|
||||
),
|
||||
expectedError: "invalid filter 'is-automated",
|
||||
},
|
||||
{
|
||||
filtersArgs: filters.NewArgs(filters.Arg("is-official", "invalid")),
|
||||
expectedError: "invalid filter 'is-official=[invalid]'",
|
||||
},
|
||||
{
|
||||
filtersArgs: filters.NewArgs(
|
||||
filters.Arg("is-official", "true"),
|
||||
filters.Arg("is-official", "false"),
|
||||
),
|
||||
expectedError: "invalid filter 'is-official",
|
||||
},
|
||||
{
|
||||
filtersArgs: filters.NewArgs(filters.Arg("stars", "invalid")),
|
||||
expectedError: "invalid filter 'stars=invalid'",
|
||||
},
|
||||
{
|
||||
filtersArgs: filters.NewArgs(
|
||||
filters.Arg("stars", "1"),
|
||||
filters.Arg("stars", "invalid"),
|
||||
),
|
||||
expectedError: "invalid filter 'stars=invalid'",
|
||||
},
|
||||
}
|
||||
for _, tc := range errorCases {
|
||||
t.Run(tc.expectedError, func(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !tc.shouldReturnError {
|
||||
t.Errorf("unexpected HTTP request")
|
||||
}
|
||||
http.Error(w, "no search for you", http.StatusInternalServerError)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
// Construct the search term by cutting the 'http://' prefix off srv.URL.
|
||||
term := srv.URL[7:] + "/term"
|
||||
|
||||
reg, err := NewService(ServiceOptions{})
|
||||
assert.NilError(t, err)
|
||||
_, err = reg.Search(context.Background(), tc.filtersArgs, term, 0, nil, map[string][]string{})
|
||||
assert.ErrorContains(t, err, tc.expectedError)
|
||||
if tc.shouldReturnError {
|
||||
assert.Check(t, cerrdefs.IsUnknown(err), "got: %T: %v", err, err)
|
||||
return
|
||||
}
|
||||
assert.Check(t, cerrdefs.IsInvalidArgument(err), "got: %T: %v", err, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearch(t *testing.T) {
|
||||
const term = "term"
|
||||
successCases := []struct {
|
||||
name string
|
||||
filtersArgs filters.Args
|
||||
registryResults []registry.SearchResult
|
||||
expectedResults []registry.SearchResult
|
||||
}{
|
||||
{
|
||||
name: "empty results",
|
||||
registryResults: []registry.SearchResult{},
|
||||
expectedResults: []registry.SearchResult{},
|
||||
},
|
||||
{
|
||||
name: "no filter",
|
||||
registryResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
},
|
||||
},
|
||||
expectedResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "is-automated=true, no results",
|
||||
filtersArgs: filters.NewArgs(filters.Arg("is-automated", "true")),
|
||||
registryResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
},
|
||||
},
|
||||
expectedResults: []registry.SearchResult{},
|
||||
},
|
||||
{
|
||||
name: "is-automated=true",
|
||||
filtersArgs: filters.NewArgs(filters.Arg("is-automated", "true")),
|
||||
registryResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
|
||||
},
|
||||
},
|
||||
expectedResults: []registry.SearchResult{},
|
||||
},
|
||||
{
|
||||
name: "is-automated=false, IsAutomated reset to false",
|
||||
filtersArgs: filters.NewArgs(filters.Arg("is-automated", "false")),
|
||||
registryResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
|
||||
},
|
||||
},
|
||||
expectedResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
IsAutomated: false, //nolint:staticcheck // ignore SA1019 (field is deprecated).
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "is-automated=false",
|
||||
filtersArgs: filters.NewArgs(filters.Arg("is-automated", "false")),
|
||||
registryResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
},
|
||||
},
|
||||
expectedResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "is-official=true, no results",
|
||||
filtersArgs: filters.NewArgs(filters.Arg("is-official", "true")),
|
||||
registryResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
},
|
||||
},
|
||||
expectedResults: []registry.SearchResult{},
|
||||
},
|
||||
{
|
||||
name: "is-official=true",
|
||||
filtersArgs: filters.NewArgs(filters.Arg("is-official", "true")),
|
||||
registryResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
IsOfficial: true,
|
||||
},
|
||||
},
|
||||
expectedResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
IsOfficial: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "is-official=false, no results",
|
||||
filtersArgs: filters.NewArgs(filters.Arg("is-official", "false")),
|
||||
registryResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
IsOfficial: true,
|
||||
},
|
||||
},
|
||||
expectedResults: []registry.SearchResult{},
|
||||
},
|
||||
{
|
||||
name: "is-official=false",
|
||||
filtersArgs: filters.NewArgs(filters.Arg("is-official", "false")),
|
||||
registryResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
IsOfficial: false,
|
||||
},
|
||||
},
|
||||
expectedResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
IsOfficial: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "stars=0",
|
||||
filtersArgs: filters.NewArgs(filters.Arg("stars", "0")),
|
||||
registryResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
StarCount: 0,
|
||||
},
|
||||
},
|
||||
expectedResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
StarCount: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "stars=0, no results",
|
||||
filtersArgs: filters.NewArgs(filters.Arg("stars", "1")),
|
||||
registryResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name",
|
||||
Description: "description",
|
||||
StarCount: 0,
|
||||
},
|
||||
},
|
||||
expectedResults: []registry.SearchResult{},
|
||||
},
|
||||
{
|
||||
name: "stars=1",
|
||||
filtersArgs: filters.NewArgs(filters.Arg("stars", "1")),
|
||||
registryResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name0",
|
||||
Description: "description0",
|
||||
StarCount: 0,
|
||||
},
|
||||
{
|
||||
Name: "name1",
|
||||
Description: "description1",
|
||||
StarCount: 1,
|
||||
},
|
||||
},
|
||||
expectedResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name1",
|
||||
Description: "description1",
|
||||
StarCount: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "stars=1, is-official=true, is-automated=true",
|
||||
filtersArgs: filters.NewArgs(
|
||||
filters.Arg("stars", "1"),
|
||||
filters.Arg("is-official", "true"),
|
||||
filters.Arg("is-automated", "true"),
|
||||
),
|
||||
registryResults: []registry.SearchResult{
|
||||
{
|
||||
Name: "name0",
|
||||
Description: "description0",
|
||||
StarCount: 0,
|
||||
IsOfficial: true,
|
||||
IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
|
||||
},
|
||||
{
|
||||
Name: "name1",
|
||||
Description: "description1",
|
||||
StarCount: 1,
|
||||
IsOfficial: false,
|
||||
IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
|
||||
},
|
||||
{
|
||||
Name: "name2",
|
||||
Description: "description2",
|
||||
StarCount: 1,
|
||||
IsOfficial: true,
|
||||
},
|
||||
{
|
||||
Name: "name3",
|
||||
Description: "description3",
|
||||
StarCount: 2,
|
||||
IsOfficial: true,
|
||||
IsAutomated: true, //nolint:staticcheck // ignore SA1019 (field is deprecated).
|
||||
},
|
||||
},
|
||||
expectedResults: []registry.SearchResult{},
|
||||
},
|
||||
}
|
||||
for _, tc := range successCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-type", "application/json")
|
||||
json.NewEncoder(w).Encode(registry.SearchResults{
|
||||
Query: term,
|
||||
NumResults: len(tc.registryResults),
|
||||
Results: tc.registryResults,
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
// Construct the search term by cutting the 'http://' prefix off srv.URL.
|
||||
searchTerm := srv.URL[7:] + "/" + term
|
||||
|
||||
reg, err := NewService(ServiceOptions{})
|
||||
assert.NilError(t, err)
|
||||
results, err := reg.Search(context.Background(), tc.filtersArgs, searchTerm, 0, nil, map[string][]string{})
|
||||
assert.NilError(t, err)
|
||||
assert.DeepEqual(t, results, tc.expectedResults)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -108,17 +108,6 @@ func (s *Service) Auth(ctx context.Context, authConfig *registry.AuthConfig, use
|
|||
return "", "", lastErr
|
||||
}
|
||||
|
||||
// ResolveRepository splits a repository name into its components
|
||||
// and configuration of the associated registry.
|
||||
//
|
||||
// Deprecated: this function was only used internally and is no longer used. It will be removed in the next release.
|
||||
func (s *Service) ResolveRepository(name reference.Named) (*RepositoryInfo, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
// TODO(thaJeztah): remove error return as it's no longer used.
|
||||
return newRepositoryInfo(s.config, name), nil
|
||||
}
|
||||
|
||||
// ResolveAuthConfig looks up authentication for the given reference from the
|
||||
// given authConfigs.
|
||||
//
|
||||
|
|
@ -139,12 +128,9 @@ func (s *Service) ResolveAuthConfig(authConfigs map[string]registry.AuthConfig,
|
|||
|
||||
// APIEndpoint represents a remote API endpoint
|
||||
type APIEndpoint struct {
|
||||
Mirror bool
|
||||
URL *url.URL
|
||||
AllowNondistributableArtifacts bool // Deprecated: non-distributable artifacts are deprecated and enabled by default. This field will be removed in the next release.
|
||||
Official bool // Deprecated: this field was only used internally, and will be removed in the next release.
|
||||
TrimHostname bool // Deprecated: hostname is now trimmed unconditionally for remote names. This field will be removed in the next release.
|
||||
TLSConfig *tls.Config
|
||||
Mirror bool
|
||||
URL *url.URL
|
||||
TLSConfig *tls.Config
|
||||
}
|
||||
|
||||
// LookupPullEndpoints creates a list of v2 endpoints to try to pull from, in order of preference.
|
||||
|
|
@ -37,7 +37,6 @@ func (s *Service) lookupV2Endpoints(ctx context.Context, hostname string, includ
|
|||
}
|
||||
endpoints = append(endpoints, APIEndpoint{
|
||||
URL: DefaultV2Registry,
|
||||
Official: true,
|
||||
TLSConfig: tlsconfig.ServerDefault(),
|
||||
})
|
||||
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
)
|
||||
|
||||
// RepositoryInfo describes a repository
|
||||
type RepositoryInfo struct {
|
||||
Name reference.Named
|
||||
// Index points to registry information
|
||||
Index *registry.IndexInfo
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ go 1.23.0
|
|||
require (
|
||||
dario.cat/mergo v1.0.1
|
||||
github.com/containerd/errdefs v1.0.0
|
||||
github.com/containerd/log v0.1.0
|
||||
github.com/containerd/platforms v1.0.0-rc.1
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7
|
||||
github.com/creack/pty v1.1.24
|
||||
|
|
@ -47,6 +48,7 @@ require (
|
|||
github.com/theupdateframework/notary v0.7.1-0.20210315103452-bf96a202a09a
|
||||
github.com/tonistiigi/go-rosetta v0.0.0-20220804170347-3f4430f2d346
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0
|
||||
go.opentelemetry.io/otel v1.35.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.35.0
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0
|
||||
|
|
@ -70,7 +72,6 @@ require (
|
|||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect
|
||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
|
||||
github.com/docker/go-metrics v0.0.1 // indirect
|
||||
|
|
@ -97,7 +98,6 @@ require (
|
|||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
|
||||
go.etcd.io/etcd/raft/v3 v3.5.16 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
package homedir
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/user"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// Get returns the home directory of the current user with the help of
|
||||
// environment variables depending on the target operating system.
|
||||
// Returned path should be used with "path/filepath" to form new paths.
|
||||
//
|
||||
// On non-Windows platforms, it falls back to nss lookups, if the home
|
||||
// directory cannot be obtained from environment-variables.
|
||||
//
|
||||
// If linking statically with cgo enabled against glibc, ensure the
|
||||
// osusergo build tag is used.
|
||||
//
|
||||
// If needing to do nss lookups, do not disable cgo or set osusergo.
|
||||
func Get() string {
|
||||
home, _ := os.UserHomeDir()
|
||||
if home == "" && runtime.GOOS != "windows" {
|
||||
if u, err := user.Current(); err == nil {
|
||||
return u.HomeDir
|
||||
}
|
||||
}
|
||||
return home
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
package homedir
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetRuntimeDir returns XDG_RUNTIME_DIR.
|
||||
// XDG_RUNTIME_DIR is typically configured via pam_systemd.
|
||||
// GetRuntimeDir returns non-nil error if XDG_RUNTIME_DIR is not set.
|
||||
//
|
||||
// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html
|
||||
func GetRuntimeDir() (string, error) {
|
||||
if xdgRuntimeDir := os.Getenv("XDG_RUNTIME_DIR"); xdgRuntimeDir != "" {
|
||||
return xdgRuntimeDir, nil
|
||||
}
|
||||
return "", errors.New("could not get XDG_RUNTIME_DIR")
|
||||
}
|
||||
|
||||
// StickRuntimeDirContents sets the sticky bit on files that are under
|
||||
// XDG_RUNTIME_DIR, so that the files won't be periodically removed by the system.
|
||||
//
|
||||
// StickyRuntimeDir returns slice of sticked files.
|
||||
// StickyRuntimeDir returns nil error if XDG_RUNTIME_DIR is not set.
|
||||
//
|
||||
// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html
|
||||
func StickRuntimeDirContents(files []string) ([]string, error) {
|
||||
runtimeDir, err := GetRuntimeDir()
|
||||
if err != nil {
|
||||
// ignore error if runtimeDir is empty
|
||||
return nil, nil
|
||||
}
|
||||
runtimeDir, err = filepath.Abs(runtimeDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var sticked []string
|
||||
for _, f := range files {
|
||||
f, err = filepath.Abs(f)
|
||||
if err != nil {
|
||||
return sticked, err
|
||||
}
|
||||
if strings.HasPrefix(f, runtimeDir+"/") {
|
||||
if err = stick(f); err != nil {
|
||||
return sticked, err
|
||||
}
|
||||
sticked = append(sticked, f)
|
||||
}
|
||||
}
|
||||
return sticked, nil
|
||||
}
|
||||
|
||||
func stick(f string) error {
|
||||
st, err := os.Stat(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m := st.Mode()
|
||||
m |= os.ModeSticky
|
||||
return os.Chmod(f, m)
|
||||
}
|
||||
|
||||
// GetDataHome returns XDG_DATA_HOME.
|
||||
// GetDataHome returns $HOME/.local/share and nil error if XDG_DATA_HOME is not set.
|
||||
// If HOME and XDG_DATA_HOME are not set, getpwent(3) is consulted to determine the users home directory.
|
||||
//
|
||||
// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html
|
||||
func GetDataHome() (string, error) {
|
||||
if xdgDataHome := os.Getenv("XDG_DATA_HOME"); xdgDataHome != "" {
|
||||
return xdgDataHome, nil
|
||||
}
|
||||
home := Get()
|
||||
if home == "" {
|
||||
return "", errors.New("could not get either XDG_DATA_HOME or HOME")
|
||||
}
|
||||
return filepath.Join(home, ".local", "share"), nil
|
||||
}
|
||||
|
||||
// GetConfigHome returns XDG_CONFIG_HOME.
|
||||
// GetConfigHome returns $HOME/.config and nil error if XDG_CONFIG_HOME is not set.
|
||||
// If HOME and XDG_CONFIG_HOME are not set, getpwent(3) is consulted to determine the users home directory.
|
||||
//
|
||||
// See also https://standards.freedesktop.org/basedir-spec/latest/ar01s03.html
|
||||
func GetConfigHome() (string, error) {
|
||||
if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" {
|
||||
return xdgConfigHome, nil
|
||||
}
|
||||
home := Get()
|
||||
if home == "" {
|
||||
return "", errors.New("could not get either XDG_CONFIG_HOME or HOME")
|
||||
}
|
||||
return filepath.Join(home, ".config"), nil
|
||||
}
|
||||
|
||||
// GetLibHome returns $HOME/.local/lib
|
||||
// If HOME is not set, getpwent(3) is consulted to determine the users home directory.
|
||||
func GetLibHome() (string, error) {
|
||||
home := Get()
|
||||
if home == "" {
|
||||
return "", errors.New("could not get HOME")
|
||||
}
|
||||
return filepath.Join(home, ".local/lib"), nil
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
//go:build !linux
|
||||
|
||||
package homedir
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// GetRuntimeDir is unsupported on non-linux system.
|
||||
func GetRuntimeDir() (string, error) {
|
||||
return "", errors.New("homedir.GetRuntimeDir() is not supported on this system")
|
||||
}
|
||||
|
||||
// StickRuntimeDirContents is unsupported on non-linux system.
|
||||
func StickRuntimeDirContents(files []string) ([]string, error) {
|
||||
return nil, errors.New("homedir.StickRuntimeDirContents() is not supported on this system")
|
||||
}
|
||||
|
||||
// GetDataHome is unsupported on non-linux system.
|
||||
func GetDataHome() (string, error) {
|
||||
return "", errors.New("homedir.GetDataHome() is not supported on this system")
|
||||
}
|
||||
|
||||
// GetConfigHome is unsupported on non-linux system.
|
||||
func GetConfigHome() (string, error) {
|
||||
return "", errors.New("homedir.GetConfigHome() is not supported on this system")
|
||||
}
|
||||
|
||||
// GetLibHome is unsupported on non-linux system.
|
||||
func GetLibHome() (string, error) {
|
||||
return "", errors.New("homedir.GetLibHome() is not supported on this system")
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
//go:build !windows
|
||||
|
||||
package registry
|
||||
|
||||
// defaultCertsDir is the platform-specific default directory where certificates
|
||||
// are stored. On Linux, it may be overridden through certsDir, for example, when
|
||||
// running in rootless mode.
|
||||
const defaultCertsDir = "/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,20 +0,0 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// defaultCertsDir is the platform-specific default directory where certificates
|
||||
// are stored. On Linux, it may be overridden through certsDir, for example, when
|
||||
// running in rootless mode.
|
||||
var defaultCertsDir = 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.ReplaceAll(s, ":", ""))
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/docker/api/types/registry"
|
||||
)
|
||||
|
||||
// RepositoryInfo describes a repository
|
||||
type RepositoryInfo struct {
|
||||
Name reference.Named
|
||||
// Index points to registry information
|
||||
Index *registry.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.
|
||||
//
|
||||
// Deprecated: this field is no longer used and will be removed in the next release. The information captured in this field can be obtained from the [Name] field instead.
|
||||
Official bool
|
||||
// Class represents the class of the repository, such as "plugin"
|
||||
// or "image".
|
||||
//
|
||||
// Deprecated: this field is no longer used, and will be removed in the next release.
|
||||
Class string
|
||||
}
|
||||
|
|
@ -92,12 +92,10 @@ github.com/docker/docker/api/types/volume
|
|||
github.com/docker/docker/client
|
||||
github.com/docker/docker/internal/lazyregexp
|
||||
github.com/docker/docker/internal/multierror
|
||||
github.com/docker/docker/pkg/homedir
|
||||
github.com/docker/docker/pkg/jsonmessage
|
||||
github.com/docker/docker/pkg/progress
|
||||
github.com/docker/docker/pkg/stdcopy
|
||||
github.com/docker/docker/pkg/streamformatter
|
||||
github.com/docker/docker/registry
|
||||
# github.com/docker/docker-credential-helpers v0.9.3
|
||||
## explicit; go 1.21
|
||||
github.com/docker/docker-credential-helpers/client
|
||||
|
|
|
|||
Loading…
Reference in New Issue