mirror of https://github.com/docker/cli.git
internal/registry: remove dead code
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
(cherry picked from commit 7716219e17)
Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
parent
8b9baffdf7
commit
ecd54bc6dd
|
|
@ -127,44 +127,6 @@ func v2AuthHTTPClient(endpoint *url.URL, authTransport http.RoundTripper, modifi
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConvertToHostname normalizes a registry URL which has http|https prepended
|
|
||||||
// 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(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
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResolveAuthConfig matches an auth configuration to a server address or a URL
|
|
||||||
func ResolveAuthConfig(authConfigs map[string]registry.AuthConfig, index *registry.IndexInfo) registry.AuthConfig {
|
|
||||||
configKey := GetAuthConfigKey(index)
|
|
||||||
// First try the happy case
|
|
||||||
if c, found := authConfigs[configKey]; found || index.Official {
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Maybe they have a legacy config file, we will iterate the keys converting
|
|
||||||
// them to the new format and testing
|
|
||||||
for registryURL, ac := range authConfigs {
|
|
||||||
if configKey == ConvertToHostname(registryURL) {
|
|
||||||
return ac
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// When all else fails, return an empty auth config
|
|
||||||
return registry.AuthConfig{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// PingResponseError is used when the response from a ping
|
// PingResponseError is used when the response from a ping
|
||||||
// was received but invalid.
|
// was received but invalid.
|
||||||
type PingResponseError struct {
|
type PingResponseError struct {
|
||||||
|
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -106,19 +106,6 @@ func newServiceConfig(options ServiceOptions) (*serviceConfig, error) {
|
||||||
return config, nil
|
return config, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy constructs a new ServiceConfig with a copy of the configuration in config.
|
|
||||||
func (config *serviceConfig) copy() *registry.ServiceConfig {
|
|
||||||
ic := make(map[string]*registry.IndexInfo)
|
|
||||||
for key, value := range config.IndexConfigs {
|
|
||||||
ic[key] = value
|
|
||||||
}
|
|
||||||
return ®istry.ServiceConfig{
|
|
||||||
InsecureRegistryCIDRs: append([]*registry.NetIPNet(nil), config.InsecureRegistryCIDRs...),
|
|
||||||
IndexConfigs: ic,
|
|
||||||
Mirrors: append([]string(nil), config.Mirrors...),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadMirrors loads mirrors to config, after removing duplicates.
|
// loadMirrors loads mirrors to config, after removing duplicates.
|
||||||
// Returns an error if mirrors contains an invalid mirror.
|
// Returns an error if mirrors contains an invalid mirror.
|
||||||
func (config *serviceConfig) loadMirrors(mirrors []string) error {
|
func (config *serviceConfig) loadMirrors(mirrors []string) error {
|
||||||
|
|
@ -320,18 +307,12 @@ func ValidateIndexName(val string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeIndexName(val string) string {
|
func normalizeIndexName(val string) string {
|
||||||
// TODO(thaJeztah): consider normalizing other known options, such as "(https://)registry-1.docker.io", "https://index.docker.io/v1/".
|
|
||||||
// TODO: upstream this to check to reference package
|
|
||||||
if val == "index.docker.io" {
|
if val == "index.docker.io" {
|
||||||
return "docker.io"
|
return "docker.io"
|
||||||
}
|
}
|
||||||
return val
|
return val
|
||||||
}
|
}
|
||||||
|
|
||||||
func hasScheme(reposName string) bool {
|
|
||||||
return strings.Contains(reposName, "://")
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateHostPort(s string) error {
|
func validateHostPort(s string) error {
|
||||||
// Split host and port, and in case s can not be split, assume host only
|
// Split host and port, and in case s can not be split, assume host only
|
||||||
host, port, err := net.SplitHostPort(s)
|
host, port, err := net.SplitHostPort(s)
|
||||||
|
|
@ -356,32 +337,6 @@ func validateHostPort(s string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// newIndexInfo returns IndexInfo configuration from indexName
|
|
||||||
func newIndexInfo(config *serviceConfig, indexName string) *registry.IndexInfo {
|
|
||||||
indexName = normalizeIndexName(indexName)
|
|
||||||
|
|
||||||
// Return any configured index info, first.
|
|
||||||
if index, ok := config.IndexConfigs[indexName]; ok {
|
|
||||||
return index
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct a non-configured index info.
|
|
||||||
return ®istry.IndexInfo{
|
|
||||||
Name: indexName,
|
|
||||||
Mirrors: []string{},
|
|
||||||
Secure: config.isSecureIndex(indexName),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAuthConfigKey special-cases using the full index address of the official
|
|
||||||
// index as the AuthConfig key, and uses the (host)name[:port] for private indexes.
|
|
||||||
func GetAuthConfigKey(index *registry.IndexInfo) string {
|
|
||||||
if index.Official {
|
|
||||||
return IndexServer
|
|
||||||
}
|
|
||||||
return index.Name
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseRepositoryInfo performs the breakdown of a repository name into a
|
// ParseRepositoryInfo performs the breakdown of a repository name into a
|
||||||
// [RepositoryInfo], but lacks registry configuration.
|
// [RepositoryInfo], but lacks registry configuration.
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -49,19 +49,3 @@ func (invalidParameterErr) InvalidParameter() {}
|
||||||
func (e invalidParameterErr) Unwrap() error {
|
func (e invalidParameterErr) Unwrap() error {
|
||||||
return e.error
|
return e.error
|
||||||
}
|
}
|
||||||
|
|
||||||
type systemErr struct{ error }
|
|
||||||
|
|
||||||
func (systemErr) System() {}
|
|
||||||
|
|
||||||
func (e systemErr) Unwrap() error {
|
|
||||||
return e.error
|
|
||||||
}
|
|
||||||
|
|
||||||
type errUnknown struct{ error }
|
|
||||||
|
|
||||||
func (errUnknown) Unknown() {}
|
|
||||||
|
|
||||||
func (e errUnknown) Unwrap() error {
|
|
||||||
return e.error
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,7 @@ import (
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var testHTTPServer *httptest.Server
|
||||||
testHTTPServer *httptest.Server
|
|
||||||
testHTTPSServer *httptest.Server
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
r := http.NewServeMux()
|
r := http.NewServeMux()
|
||||||
|
|
@ -29,7 +26,6 @@ func init() {
|
||||||
r.HandleFunc("/v2/version", handlerGetPing)
|
r.HandleFunc("/v2/version", handlerGetPing)
|
||||||
|
|
||||||
testHTTPServer = httptest.NewServer(handlerAccessLog(r))
|
testHTTPServer = httptest.NewServer(handlerAccessLog(r))
|
||||||
testHTTPSServer = httptest.NewTLSServer(handlerAccessLog(r))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlerAccessLog(handler http.Handler) http.Handler {
|
func handlerAccessLog(handler http.Handler) http.Handler {
|
||||||
|
|
@ -44,30 +40,6 @@ func makeURL(req string) string {
|
||||||
return testHTTPServer.URL + req
|
return testHTTPServer.URL + req
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeHTTPSURL(req string) string {
|
|
||||||
return testHTTPSServer.URL + req
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeIndex(req string) *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) {
|
func writeHeaders(w http.ResponseWriter) {
|
||||||
h := w.Header()
|
h := w.Header()
|
||||||
h.Add("Server", "docker-tests/mock")
|
h.Add("Server", "docker-tests/mock")
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,23 @@ func overrideLookupIP(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newIndexInfo returns IndexInfo configuration from indexName
|
||||||
|
func newIndexInfo(config *serviceConfig, indexName string) *registry.IndexInfo {
|
||||||
|
indexName = normalizeIndexName(indexName)
|
||||||
|
|
||||||
|
// Return any configured index info, first.
|
||||||
|
if index, ok := config.IndexConfigs[indexName]; ok {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct a non-configured index info.
|
||||||
|
return ®istry.IndexInfo{
|
||||||
|
Name: indexName,
|
||||||
|
Mirrors: []string{},
|
||||||
|
Secure: config.isSecureIndex(indexName),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseRepositoryInfo(t *testing.T) {
|
func TestParseRepositoryInfo(t *testing.T) {
|
||||||
type staticRepositoryInfo struct {
|
type staticRepositoryInfo struct {
|
||||||
Index *registry.IndexInfo
|
Index *registry.IndexInfo
|
||||||
|
|
|
||||||
|
|
@ -1,170 +0,0 @@
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/containerd/log"
|
|
||||||
"github.com/docker/distribution/registry/client/auth"
|
|
||||||
"github.com/docker/docker/api/types/filters"
|
|
||||||
"github.com/docker/docker/api/types/registry"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
var acceptedSearchFilterTags = map[string]bool{
|
|
||||||
"is-automated": true, // Deprecated: the "is_automated" field is deprecated and will always be false in the future.
|
|
||||||
"is-official": true,
|
|
||||||
"stars": true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search queries the public registry for repositories matching the specified
|
|
||||||
// search term and filters.
|
|
||||||
func (s *Service) Search(ctx context.Context, searchFilters filters.Args, term string, limit int, authConfig *registry.AuthConfig, headers map[string][]string) ([]registry.SearchResult, error) {
|
|
||||||
if err := searchFilters.Validate(acceptedSearchFilterTags); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
isAutomated, err := searchFilters.GetBoolOrDefault("is-automated", false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// "is-automated" is deprecated and filtering for `true` will yield no results.
|
|
||||||
if isAutomated {
|
|
||||||
return []registry.SearchResult{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
isOfficial, err := searchFilters.GetBoolOrDefault("is-official", false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
hasStarFilter := 0
|
|
||||||
if searchFilters.Contains("stars") {
|
|
||||||
hasStars := searchFilters.Get("stars")
|
|
||||||
for _, hasStar := range hasStars {
|
|
||||||
iHasStar, err := strconv.Atoi(hasStar)
|
|
||||||
if err != nil {
|
|
||||||
return nil, invalidParameterErr{errors.Wrapf(err, "invalid filter 'stars=%s'", hasStar)}
|
|
||||||
}
|
|
||||||
if iHasStar > hasStarFilter {
|
|
||||||
hasStarFilter = iHasStar
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unfilteredResult, err := s.searchUnfiltered(ctx, term, limit, authConfig, headers)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
filteredResults := []registry.SearchResult{}
|
|
||||||
for _, result := range unfilteredResult.Results {
|
|
||||||
if searchFilters.Contains("is-official") {
|
|
||||||
if isOfficial != result.IsOfficial {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if searchFilters.Contains("stars") {
|
|
||||||
if result.StarCount < hasStarFilter {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// "is-automated" is deprecated and the value in Docker Hub search
|
|
||||||
// results is untrustworthy. Force it to false so as to not mislead our
|
|
||||||
// clients.
|
|
||||||
result.IsAutomated = false //nolint:staticcheck // ignore SA1019 (field is deprecated)
|
|
||||||
filteredResults = append(filteredResults, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
return filteredResults, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) searchUnfiltered(ctx context.Context, term string, limit int, authConfig *registry.AuthConfig, headers http.Header) (*registry.SearchResults, error) {
|
|
||||||
if hasScheme(term) {
|
|
||||||
return nil, invalidParamf("invalid repository name: repository name (%s) should not have a scheme", term)
|
|
||||||
}
|
|
||||||
|
|
||||||
indexName, remoteName := splitReposSearchTerm(term)
|
|
||||||
|
|
||||||
// Search is a long-running operation, just lock s.config to avoid block others.
|
|
||||||
s.mu.RLock()
|
|
||||||
index := newIndexInfo(s.config, indexName)
|
|
||||||
s.mu.RUnlock()
|
|
||||||
if index.Official {
|
|
||||||
// If pull "library/foo", it's stored locally under "foo"
|
|
||||||
remoteName = strings.TrimPrefix(remoteName, "library/")
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoint, err := newV1Endpoint(ctx, index, headers)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var client *http.Client
|
|
||||||
if authConfig != nil && authConfig.IdentityToken != "" && authConfig.Username != "" {
|
|
||||||
creds := NewStaticCredentialStore(authConfig)
|
|
||||||
|
|
||||||
// TODO(thaJeztah); is there a reason not to include other headers here? (originally added in 19d48f0b8ba59eea9f2cac4ad1c7977712a6b7ac)
|
|
||||||
modifiers := Headers(headers.Get("User-Agent"), nil)
|
|
||||||
v2Client, err := v2AuthHTTPClient(endpoint.URL, endpoint.client.Transport, modifiers, creds, []auth.Scope{
|
|
||||||
auth.RegistryScope{Name: "catalog", Actions: []string{"search"}},
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// Copy non transport http client features
|
|
||||||
v2Client.Timeout = endpoint.client.Timeout
|
|
||||||
v2Client.CheckRedirect = endpoint.client.CheckRedirect
|
|
||||||
v2Client.Jar = endpoint.client.Jar
|
|
||||||
|
|
||||||
log.G(ctx).Debugf("using v2 client for search to %s", endpoint.URL)
|
|
||||||
client = v2Client
|
|
||||||
} else {
|
|
||||||
client = endpoint.client
|
|
||||||
if err := authorizeClient(ctx, client, authConfig, endpoint); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newSession(client, endpoint).searchRepositories(ctx, remoteName, limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
// splitReposSearchTerm breaks a search term into an index name and remote name
|
|
||||||
func splitReposSearchTerm(reposName string) (string, string) {
|
|
||||||
nameParts := strings.SplitN(reposName, "/", 2)
|
|
||||||
if len(nameParts) == 1 || (!strings.Contains(nameParts[0], ".") &&
|
|
||||||
!strings.Contains(nameParts[0], ":") && nameParts[0] != "localhost") {
|
|
||||||
// This is a Docker Hub repository (ex: samalba/hipache or ubuntu),
|
|
||||||
// use the default Docker Hub registry (docker.io)
|
|
||||||
return IndexName, reposName
|
|
||||||
}
|
|
||||||
return nameParts[0], nameParts[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseSearchIndexInfo will use repository name to get back an indexInfo.
|
|
||||||
//
|
|
||||||
// TODO(thaJeztah) this function is only used by the CLI, and used to get
|
|
||||||
// information of the registry (to provide credentials if needed). We should
|
|
||||||
// move this function (or equivalent) to the CLI, as it's doing too much just
|
|
||||||
// for that.
|
|
||||||
func ParseSearchIndexInfo(reposName string) (*registry.IndexInfo, error) {
|
|
||||||
indexName, _ := splitReposSearchTerm(reposName)
|
|
||||||
indexName = normalizeIndexName(indexName)
|
|
||||||
if indexName == IndexName {
|
|
||||||
return ®istry.IndexInfo{
|
|
||||||
Name: IndexName,
|
|
||||||
Mirrors: []string{},
|
|
||||||
Secure: true,
|
|
||||||
Official: true,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return ®istry.IndexInfo{
|
|
||||||
Name: indexName,
|
|
||||||
Mirrors: []string{},
|
|
||||||
Secure: !isInsecure(indexName),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,213 +0,0 @@
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/containerd/log"
|
|
||||||
"github.com/docker/distribution/registry/client/transport"
|
|
||||||
"github.com/docker/docker/api/types/registry"
|
|
||||||
)
|
|
||||||
|
|
||||||
// v1PingResult contains the information returned when pinging a registry. It
|
|
||||||
// indicates whether the registry claims to be a standalone registry.
|
|
||||||
type v1PingResult struct {
|
|
||||||
// Standalone is set to true if the registry indicates it is a
|
|
||||||
// standalone registry in the X-Docker-Registry-Standalone
|
|
||||||
// header
|
|
||||||
Standalone bool `json:"standalone"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// v1Endpoint stores basic information about a V1 registry endpoint.
|
|
||||||
type v1Endpoint struct {
|
|
||||||
client *http.Client
|
|
||||||
URL *url.URL
|
|
||||||
IsSecure bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// newV1Endpoint parses the given address to return a registry endpoint.
|
|
||||||
// TODO: remove. This is only used by search.
|
|
||||||
func newV1Endpoint(ctx context.Context, index *registry.IndexInfo, headers http.Header) (*v1Endpoint, error) {
|
|
||||||
tlsConfig, err := newTLSConfig(ctx, index.Name, index.Secure)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoint, err := newV1EndpointFromStr(GetAuthConfigKey(index), tlsConfig, headers)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if endpoint.String() == IndexServer {
|
|
||||||
// Skip the check, we know this one is valid
|
|
||||||
// (and we never want to fall back to http in case of error)
|
|
||||||
return endpoint, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try HTTPS ping to registry
|
|
||||||
endpoint.URL.Scheme = "https"
|
|
||||||
if _, err := endpoint.ping(ctx); err != nil {
|
|
||||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
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.
|
|
||||||
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.
|
|
||||||
log.G(ctx).WithError(err).Debugf("error from registry %q marked as insecure - insecurely falling back to HTTP", endpoint)
|
|
||||||
endpoint.URL.Scheme = "http"
|
|
||||||
if _, err2 := endpoint.ping(ctx); err2 != nil {
|
|
||||||
return nil, invalidParamf("invalid registry endpoint %q. HTTPS attempt: %v. HTTP attempt: %v", endpoint, err, err2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return endpoint, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// trimV1Address trims the "v1" version suffix off the address and returns
|
|
||||||
// the trimmed address. It returns an error on "v2" endpoints.
|
|
||||||
func trimV1Address(address string) (string, error) {
|
|
||||||
trimmed := strings.TrimSuffix(address, "/")
|
|
||||||
if strings.HasSuffix(trimmed, "/v2") {
|
|
||||||
return "", invalidParamf("search is not supported on v2 endpoints: %s", address)
|
|
||||||
}
|
|
||||||
return strings.TrimSuffix(trimmed, "/v1"), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newV1EndpointFromStr(address string, tlsConfig *tls.Config, headers http.Header) (*v1Endpoint, error) {
|
|
||||||
if !strings.HasPrefix(address, "http://") && !strings.HasPrefix(address, "https://") {
|
|
||||||
address = "https://" + address
|
|
||||||
}
|
|
||||||
|
|
||||||
address, err := trimV1Address(address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
uri, err := url.Parse(address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, invalidParam(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(tiborvass): make sure a ConnectTimeout transport is used
|
|
||||||
tr := newTransport(tlsConfig)
|
|
||||||
|
|
||||||
return &v1Endpoint{
|
|
||||||
IsSecure: tlsConfig == nil || !tlsConfig.InsecureSkipVerify,
|
|
||||||
URL: uri,
|
|
||||||
client: httpClient(transport.NewTransport(tr, Headers("", headers)...)),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the formatted URL for the root of this registry Endpoint
|
|
||||||
func (e *v1Endpoint) String() string {
|
|
||||||
return e.URL.String() + "/v1/"
|
|
||||||
}
|
|
||||||
|
|
||||||
// ping returns a v1PingResult which indicates whether the registry is standalone or not.
|
|
||||||
func (e *v1Endpoint) ping(ctx context.Context) (v1PingResult, error) {
|
|
||||||
if e.String() == IndexServer {
|
|
||||||
// Skip the check, we know this one is valid
|
|
||||||
// (and we never want to fallback to http in case of error)
|
|
||||||
return v1PingResult{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
pingURL := e.String() + "_ping"
|
|
||||||
log.G(ctx).WithField("url", pingURL).Debug("attempting v1 ping for registry endpoint")
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, pingURL, http.NoBody)
|
|
||||||
if err != nil {
|
|
||||||
return v1PingResult{}, invalidParam(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := e.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
|
||||||
return v1PingResult{}, err
|
|
||||||
}
|
|
||||||
return v1PingResult{}, invalidParam(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if v := resp.Header.Get("X-Docker-Registry-Standalone"); v != "" {
|
|
||||||
info := v1PingResult{}
|
|
||||||
// Accepted values are "1", and "true" (case-insensitive).
|
|
||||||
if v == "1" || strings.EqualFold(v, "true") {
|
|
||||||
info.Standalone = true
|
|
||||||
}
|
|
||||||
log.G(ctx).Debugf("v1PingResult.Standalone (from X-Docker-Registry-Standalone header): %t", info.Standalone)
|
|
||||||
return info, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the header is absent, we assume true for compatibility with earlier
|
|
||||||
// versions of the registry. default to true
|
|
||||||
info := v1PingResult{
|
|
||||||
Standalone: true,
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
|
|
||||||
log.G(ctx).WithError(err).Debug("error unmarshaling _ping response")
|
|
||||||
// don't stop here. Just assume sane defaults
|
|
||||||
}
|
|
||||||
|
|
||||||
log.G(ctx).Debugf("v1PingResult.Standalone: %t", info.Standalone)
|
|
||||||
return info, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// httpClient returns an HTTP client structure which uses the given transport
|
|
||||||
// and contains the necessary headers for redirected requests
|
|
||||||
func httpClient(tr http.RoundTripper) *http.Client {
|
|
||||||
return &http.Client{
|
|
||||||
Transport: tr,
|
|
||||||
CheckRedirect: addRequiredHeadersToRedirectedRequests,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func trustedLocation(req *http.Request) bool {
|
|
||||||
var (
|
|
||||||
trusteds = []string{"docker.com", "docker.io"}
|
|
||||||
hostname = strings.SplitN(req.Host, ":", 2)[0]
|
|
||||||
)
|
|
||||||
if req.URL.Scheme != "https" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, trusted := range trusteds {
|
|
||||||
if hostname == trusted || strings.HasSuffix(hostname, "."+trusted) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// addRequiredHeadersToRedirectedRequests adds the necessary redirection headers
|
|
||||||
// for redirected requests
|
|
||||||
func addRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Request) error {
|
|
||||||
if len(via) != 0 && via[0] != nil {
|
|
||||||
if trustedLocation(req) && trustedLocation(via[0]) {
|
|
||||||
req.Header = via[0].Header
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for k, v := range via[0].Header {
|
|
||||||
if k != "Authorization" {
|
|
||||||
for _, vv := range v {
|
|
||||||
req.Header.Add(k, vv)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,237 +0,0 @@
|
||||||
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'")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,248 +0,0 @@
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
// this is required for some certificates
|
|
||||||
"context"
|
|
||||||
_ "crypto/sha512"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/http/cookiejar"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/containerd/log"
|
|
||||||
"github.com/docker/docker/api/types/registry"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A session is used to communicate with a V1 registry
|
|
||||||
type session struct {
|
|
||||||
indexEndpoint *v1Endpoint
|
|
||||||
client *http.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
type authTransport struct {
|
|
||||||
base http.RoundTripper
|
|
||||||
authConfig *registry.AuthConfig
|
|
||||||
|
|
||||||
alwaysSetBasicAuth bool
|
|
||||||
token []string
|
|
||||||
|
|
||||||
mu sync.Mutex // guards modReq
|
|
||||||
modReq map[*http.Request]*http.Request // original -> modified
|
|
||||||
}
|
|
||||||
|
|
||||||
// newAuthTransport handles the auth layer when communicating with a v1 registry (private or official)
|
|
||||||
//
|
|
||||||
// For private v1 registries, set alwaysSetBasicAuth to true.
|
|
||||||
//
|
|
||||||
// For the official v1 registry, if there isn't already an Authorization header in the request,
|
|
||||||
// but there is an X-Docker-Token header set to true, then Basic Auth will be used to set the Authorization header.
|
|
||||||
// After sending the request with the provided base http.RoundTripper, if an X-Docker-Token header, representing
|
|
||||||
// a token, is present in the response, then it gets cached and sent in the Authorization header of all subsequent
|
|
||||||
// requests.
|
|
||||||
//
|
|
||||||
// If the server sends a token without the client having requested it, it is ignored.
|
|
||||||
//
|
|
||||||
// This RoundTripper also has a CancelRequest method important for correct timeout handling.
|
|
||||||
func newAuthTransport(base http.RoundTripper, authConfig *registry.AuthConfig, alwaysSetBasicAuth bool) *authTransport {
|
|
||||||
if base == nil {
|
|
||||||
base = http.DefaultTransport
|
|
||||||
}
|
|
||||||
return &authTransport{
|
|
||||||
base: base,
|
|
||||||
authConfig: authConfig,
|
|
||||||
alwaysSetBasicAuth: alwaysSetBasicAuth,
|
|
||||||
modReq: make(map[*http.Request]*http.Request),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// cloneRequest returns a clone of the provided *http.Request.
|
|
||||||
// The clone is a shallow copy of the struct and its Header map.
|
|
||||||
func cloneRequest(r *http.Request) *http.Request {
|
|
||||||
// shallow copy of the struct
|
|
||||||
r2 := new(http.Request)
|
|
||||||
*r2 = *r
|
|
||||||
// deep copy of the Header
|
|
||||||
r2.Header = make(http.Header, len(r.Header))
|
|
||||||
for k, s := range r.Header {
|
|
||||||
r2.Header[k] = append([]string(nil), s...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return r2
|
|
||||||
}
|
|
||||||
|
|
||||||
// onEOFReader wraps an io.ReadCloser and a function
|
|
||||||
// the function will run at the end of file or close the file.
|
|
||||||
type onEOFReader struct {
|
|
||||||
Rc io.ReadCloser
|
|
||||||
Fn func()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *onEOFReader) Read(p []byte) (int, error) {
|
|
||||||
n, err := r.Rc.Read(p)
|
|
||||||
if err == io.EOF {
|
|
||||||
r.runFunc()
|
|
||||||
}
|
|
||||||
return n, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the file and run the function.
|
|
||||||
func (r *onEOFReader) Close() error {
|
|
||||||
err := r.Rc.Close()
|
|
||||||
r.runFunc()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *onEOFReader) runFunc() {
|
|
||||||
if fn := r.Fn; fn != nil {
|
|
||||||
fn()
|
|
||||||
r.Fn = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// RoundTrip changes an HTTP request's headers to add the necessary
|
|
||||||
// authentication-related headers
|
|
||||||
func (tr *authTransport) RoundTrip(orig *http.Request) (*http.Response, error) {
|
|
||||||
// Authorization should not be set on 302 redirect for untrusted locations.
|
|
||||||
// This logic mirrors the behavior in addRequiredHeadersToRedirectedRequests.
|
|
||||||
// As the authorization logic is currently implemented in RoundTrip,
|
|
||||||
// a 302 redirect is detected by looking at the Referrer header as go http package adds said header.
|
|
||||||
// This is safe as Docker doesn't set Referrer in other scenarios.
|
|
||||||
if orig.Header.Get("Referer") != "" && !trustedLocation(orig) {
|
|
||||||
return tr.base.RoundTrip(orig)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := cloneRequest(orig)
|
|
||||||
tr.mu.Lock()
|
|
||||||
tr.modReq[orig] = req
|
|
||||||
tr.mu.Unlock()
|
|
||||||
|
|
||||||
if tr.alwaysSetBasicAuth {
|
|
||||||
if tr.authConfig == nil {
|
|
||||||
return nil, errors.New("unexpected error: empty auth config")
|
|
||||||
}
|
|
||||||
req.SetBasicAuth(tr.authConfig.Username, tr.authConfig.Password)
|
|
||||||
return tr.base.RoundTrip(req)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't override
|
|
||||||
if req.Header.Get("Authorization") == "" {
|
|
||||||
if req.Header.Get("X-Docker-Token") == "true" && tr.authConfig != nil && tr.authConfig.Username != "" {
|
|
||||||
req.SetBasicAuth(tr.authConfig.Username, tr.authConfig.Password)
|
|
||||||
} else if len(tr.token) > 0 {
|
|
||||||
req.Header.Set("Authorization", "Token "+strings.Join(tr.token, ","))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resp, err := tr.base.RoundTrip(req)
|
|
||||||
if err != nil {
|
|
||||||
tr.mu.Lock()
|
|
||||||
delete(tr.modReq, orig)
|
|
||||||
tr.mu.Unlock()
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(resp.Header["X-Docker-Token"]) > 0 {
|
|
||||||
tr.token = resp.Header["X-Docker-Token"]
|
|
||||||
}
|
|
||||||
resp.Body = &onEOFReader{
|
|
||||||
Rc: resp.Body,
|
|
||||||
Fn: func() {
|
|
||||||
tr.mu.Lock()
|
|
||||||
delete(tr.modReq, orig)
|
|
||||||
tr.mu.Unlock()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CancelRequest cancels an in-flight request by closing its connection.
|
|
||||||
func (tr *authTransport) CancelRequest(req *http.Request) {
|
|
||||||
type canceler interface {
|
|
||||||
CancelRequest(*http.Request)
|
|
||||||
}
|
|
||||||
if cr, ok := tr.base.(canceler); ok {
|
|
||||||
tr.mu.Lock()
|
|
||||||
modReq := tr.modReq[req]
|
|
||||||
delete(tr.modReq, req)
|
|
||||||
tr.mu.Unlock()
|
|
||||||
cr.CancelRequest(modReq)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func authorizeClient(ctx context.Context, client *http.Client, authConfig *registry.AuthConfig, endpoint *v1Endpoint) error {
|
|
||||||
var alwaysSetBasicAuth bool
|
|
||||||
|
|
||||||
// If we're working with a standalone private registry over HTTPS, send Basic Auth headers
|
|
||||||
// alongside all our requests.
|
|
||||||
if endpoint.String() != IndexServer && endpoint.URL.Scheme == "https" {
|
|
||||||
info, err := endpoint.ping(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if info.Standalone && authConfig != nil {
|
|
||||||
log.G(ctx).WithField("endpoint", endpoint.String()).Debug("Endpoint is eligible for private registry; enabling alwaysSetBasicAuth")
|
|
||||||
alwaysSetBasicAuth = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Annotate the transport unconditionally so that v2 can
|
|
||||||
// properly fallback on v1 when an image is not found.
|
|
||||||
client.Transport = newAuthTransport(client.Transport, authConfig, alwaysSetBasicAuth)
|
|
||||||
|
|
||||||
jar, err := cookiejar.New(nil)
|
|
||||||
if err != nil {
|
|
||||||
return systemErr{errors.New("cookiejar.New is not supposed to return an error")}
|
|
||||||
}
|
|
||||||
client.Jar = jar
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSession(client *http.Client, endpoint *v1Endpoint) *session {
|
|
||||||
return &session{
|
|
||||||
client: client,
|
|
||||||
indexEndpoint: endpoint,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// defaultSearchLimit is the default value for maximum number of returned search results.
|
|
||||||
const defaultSearchLimit = 25
|
|
||||||
|
|
||||||
// searchRepositories performs a search against the remote repository
|
|
||||||
func (r *session) searchRepositories(ctx context.Context, term string, limit int) (*registry.SearchResults, error) {
|
|
||||||
if limit == 0 {
|
|
||||||
limit = defaultSearchLimit
|
|
||||||
}
|
|
||||||
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(strconv.Itoa(limit))
|
|
||||||
log.G(ctx).WithField("url", u).Debug("searchRepositories")
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, http.NoBody)
|
|
||||||
if err != nil {
|
|
||||||
return nil, invalidParamWrapf(err, "error building request")
|
|
||||||
}
|
|
||||||
// Have the AuthTransport send authentication, when logged in.
|
|
||||||
req.Header.Set("X-Docker-Token", "true")
|
|
||||||
res, err := r.client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, systemErr{err}
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
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)}
|
|
||||||
}
|
|
||||||
result := ®istry.SearchResults{}
|
|
||||||
err = json.NewDecoder(res.Body).Decode(result)
|
|
||||||
if err != nil {
|
|
||||||
return nil, systemErr{errors.Wrap(err, "error decoding registry search results")}
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,418 +0,0 @@
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -6,11 +6,9 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
cerrdefs "github.com/containerd/errdefs"
|
cerrdefs "github.com/containerd/errdefs"
|
||||||
"github.com/containerd/log"
|
"github.com/containerd/log"
|
||||||
"github.com/distribution/reference"
|
|
||||||
"github.com/docker/docker/api/types/registry"
|
"github.com/docker/docker/api/types/registry"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -18,7 +16,6 @@ import (
|
||||||
// of mirrors.
|
// of mirrors.
|
||||||
type Service struct {
|
type Service struct {
|
||||||
config *serviceConfig
|
config *serviceConfig
|
||||||
mu sync.RWMutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewService returns a new instance of [Service] ready to be installed into
|
// NewService returns a new instance of [Service] ready to be installed into
|
||||||
|
|
@ -32,27 +29,6 @@ func NewService(options ServiceOptions) (*Service, error) {
|
||||||
return &Service{config: config}, err
|
return &Service{config: config}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceConfig returns a copy of the public registry service's configuration.
|
|
||||||
func (s *Service) ServiceConfig() *registry.ServiceConfig {
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
return s.config.copy()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReplaceConfig prepares a transaction which will atomically replace the
|
|
||||||
// registry service's configuration when the returned commit function is called.
|
|
||||||
func (s *Service) ReplaceConfig(options ServiceOptions) (commit func(), _ error) {
|
|
||||||
config, err := newServiceConfig(options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return func() {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
s.config = config
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auth contacts the public registry with the provided credentials,
|
// Auth contacts the public registry with the provided credentials,
|
||||||
// and returns OK if authentication was successful.
|
// and returns OK if authentication was successful.
|
||||||
// It can be used to verify the validity of a client's credentials.
|
// It can be used to verify the validity of a client's credentials.
|
||||||
|
|
@ -74,9 +50,7 @@ func (s *Service) Auth(ctx context.Context, authConfig *registry.AuthConfig, use
|
||||||
|
|
||||||
// Lookup endpoints for authentication but exclude mirrors to prevent
|
// Lookup endpoints for authentication but exclude mirrors to prevent
|
||||||
// sending credentials of the upstream registry to a mirror.
|
// sending credentials of the upstream registry to a mirror.
|
||||||
s.mu.RLock()
|
|
||||||
endpoints, err := s.lookupV2Endpoints(ctx, registryHostName, false)
|
endpoints, err := s.lookupV2Endpoints(ctx, registryHostName, false)
|
||||||
s.mu.RUnlock()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
|
|
@ -108,24 +82,6 @@ func (s *Service) Auth(ctx context.Context, authConfig *registry.AuthConfig, use
|
||||||
return "", "", lastErr
|
return "", "", lastErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolveAuthConfig looks up authentication for the given reference from the
|
|
||||||
// given authConfigs.
|
|
||||||
//
|
|
||||||
// IMPORTANT: This function is for internal use and should not be used by external projects.
|
|
||||||
func (s *Service) ResolveAuthConfig(authConfigs map[string]registry.AuthConfig, ref reference.Named) registry.AuthConfig {
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
// Simplified version of "newIndexInfo" without handling of insecure
|
|
||||||
// registries and mirrors, as we don't need that information to resolve
|
|
||||||
// the auth-config.
|
|
||||||
indexName := normalizeIndexName(reference.Domain(ref))
|
|
||||||
registryInfo, ok := s.config.IndexConfigs[indexName]
|
|
||||||
if !ok {
|
|
||||||
registryInfo = ®istry.IndexInfo{Name: indexName}
|
|
||||||
}
|
|
||||||
return ResolveAuthConfig(authConfigs, registryInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
// APIEndpoint represents a remote API endpoint
|
// APIEndpoint represents a remote API endpoint
|
||||||
type APIEndpoint struct {
|
type APIEndpoint struct {
|
||||||
Mirror bool
|
Mirror bool
|
||||||
|
|
@ -136,25 +92,11 @@ type APIEndpoint struct {
|
||||||
// LookupPullEndpoints creates a list of v2 endpoints to try to pull from, in order of preference.
|
// LookupPullEndpoints creates a list of v2 endpoints to try to pull from, in order of preference.
|
||||||
// It gives preference to mirrors over the actual registry, and HTTPS over plain HTTP.
|
// It gives preference to mirrors over the actual registry, and HTTPS over plain HTTP.
|
||||||
func (s *Service) LookupPullEndpoints(hostname string) ([]APIEndpoint, error) {
|
func (s *Service) LookupPullEndpoints(hostname string) ([]APIEndpoint, error) {
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
|
|
||||||
return s.lookupV2Endpoints(context.TODO(), hostname, true)
|
return s.lookupV2Endpoints(context.TODO(), hostname, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LookupPushEndpoints creates a list of v2 endpoints to try to push to, in order of preference.
|
// LookupPushEndpoints creates a list of v2 endpoints to try to push to, in order of preference.
|
||||||
// It gives preference to HTTPS over plain HTTP. Mirrors are not included.
|
// It gives preference to HTTPS over plain HTTP. Mirrors are not included.
|
||||||
func (s *Service) LookupPushEndpoints(hostname string) ([]APIEndpoint, error) {
|
func (s *Service) LookupPushEndpoints(hostname string) ([]APIEndpoint, error) {
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
|
|
||||||
return s.lookupV2Endpoints(context.TODO(), hostname, false)
|
return s.lookupV2Endpoints(context.TODO(), hostname, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsInsecureRegistry returns true if the registry at given host is configured as
|
|
||||||
// insecure registry.
|
|
||||||
func (s *Service) IsInsecureRegistry(host string) bool {
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
return !s.config.isSecureIndex(host)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue