Merge pull request #46360 from thaJeztah/search_split_search_service_step1

registry: split search-related code to separate files
This commit is contained in:
Sebastiaan van Stijn 2023-09-01 18:31:16 +02:00 committed by GitHub
commit 97206b7cba
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 394 additions and 393 deletions

View file

@ -149,8 +149,8 @@ func ResolveAuthConfig(authConfigs map[string]registry.AuthConfig, index *regist
// Maybe they have a legacy config file, we will iterate the keys converting
// them to the new format and testing
for registry, ac := range authConfigs {
if configKey == ConvertToHostname(registry) {
for registryURL, ac := range authConfigs {
if configKey == ConvertToHostname(registryURL) {
return ac
}
}

View file

@ -435,14 +435,3 @@ func newRepositoryInfo(config *serviceConfig, name reference.Named) (*Repository
func ParseRepositoryInfo(reposName reference.Named) (*RepositoryInfo, error) {
return newRepositoryInfo(emptyServiceConfig, reposName)
}
// 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)
return newIndexInfo(emptyServiceConfig, indexName)
}

View file

@ -1,92 +0,0 @@
package registry // import "github.com/docker/docker/registry"
import (
"net/http"
"net/http/httptest"
"net/url"
"testing"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
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 {
tc := tc
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 TestValidateEndpoint(t *testing.T) {
requireBasicAuthHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("WWW-Authenticate", `Basic realm="localhost"`)
w.WriteHeader(http.StatusUnauthorized)
})
// Make a test server which should validate as a v1 server.
testServer := httptest.NewServer(requireBasicAuthHandler)
defer testServer.Close()
testServerURL, err := url.Parse(testServer.URL)
if err != nil {
t.Fatal(err)
}
testEndpoint := v1Endpoint{
URL: testServerURL,
client: httpClient(newTransport(nil)),
}
if err = validateEndpoint(&testEndpoint); err != nil {
t.Fatal(err)
}
if testEndpoint.URL.Scheme != "http" {
t.Fatalf("expecting to validate endpoint as http, got url %s", testEndpoint.String())
}
}

View file

@ -113,51 +113,6 @@ func Headers(userAgent string, metaHeaders http.Header) []transport.RequestModif
return modifiers
}
// httpClient returns an HTTP client structure which uses the given transport
// and contains the necessary headers for redirected requests
func httpClient(transport http.RoundTripper) *http.Client {
return &http.Client{
Transport: transport,
CheckRedirect: addRequiredHeadersToRedirectedRequests,
}
}
func trustedLocation(req *http.Request) bool {
var (
trusteds = []string{"docker.com", "docker.io"}
hostname = strings.SplitN(req.Host, ":", 2)[0]
)
if req.URL.Scheme != "https" {
return false
}
for _, trusted := range trusteds {
if hostname == trusted || strings.HasSuffix(hostname, "."+trusted) {
return true
}
}
return false
}
// addRequiredHeadersToRedirectedRequests adds the necessary redirection headers
// for redirected requests
func addRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Request) error {
if 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
}
// newTransport returns a new HTTP transport. If tlsConfig is nil, it uses the
// default TLS configuration.
func newTransport(tlsConfig *tls.Config) *http.Transport {

View file

@ -1,142 +1,16 @@
package registry // import "github.com/docker/docker/registry"
import (
"net/http"
"net/http/httputil"
"os"
"strings"
"testing"
"github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/client/transport"
"github.com/docker/docker/api/types/registry"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/skip"
)
func spawnTestRegistrySession(t *testing.T) *session {
authConfig := &registry.AuthConfig{}
endpoint, err := newV1Endpoint(makeIndex("/v1/"), nil)
if err != nil {
t.Fatal(err)
}
userAgent := "docker test client"
var tr http.RoundTripper = debugTransport{newTransport(nil), t.Log}
tr = transport.NewTransport(newAuthTransport(tr, authConfig, false), Headers(userAgent, nil)...)
client := httpClient(tr)
if err := authorizeClient(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
}
func TestPingRegistryEndpoint(t *testing.T) {
skip.If(t, os.Getuid() != 0, "skipping test that requires root")
testPing := func(index *registry.IndexInfo, expectedStandalone bool, assertMessage string) {
ep, err := newV1Endpoint(index, nil)
if err != nil {
t.Fatal(err)
}
regInfo, err := ep.ping()
if err != nil {
t.Fatal(err)
}
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 TestEndpoint(t *testing.T) {
skip.If(t, os.Getuid() != 0, "skipping test that requires root")
// Simple wrapper to fail test if err != nil
expandEndpoint := func(index *registry.IndexInfo) *v1Endpoint {
endpoint, err := newV1Endpoint(index, nil)
if err != nil {
t.Fatal(err)
}
return endpoint
}
assertInsecureIndex := func(index *registry.IndexInfo) {
index.Secure = true
_, err := newV1Endpoint(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(index, nil)
assert.ErrorContains(t, err, "certificate signed by unknown authority", index.Name+": Expected cert error for secure index")
index.Secure = false
}
index := &registry.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(index, nil)
assert.Check(t, err != nil, "Expected error while expanding bad endpoint: %s", address)
}
}
func TestParseRepositoryInfo(t *testing.T) {
type staticRepositoryInfo struct {
Index *registry.IndexInfo
@ -543,83 +417,6 @@ func TestMirrorEndpointLookup(t *testing.T) {
}
}
func TestSearchRepositories(t *testing.T) {
r := spawnTestRegistrySession(t)
results, err := r.searchRepositories("fakequery", 25)
if err != nil {
t.Fatal(err)
}
if results == nil {
t.Fatal("Expected non-nil SearchResults object")
}
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 TestTrustedLocation(t *testing.T) {
for _, url := range []string{"http://example.com", "https://example.com:7777", "http://docker.io", "http://test.docker.com", "https://fakedocker.com"} {
req, _ := http.NewRequest(http.MethodGet, url, nil)
assert.Check(t, !trustedLocation(req))
}
for _, url := range []string{"https://docker.io", "https://test.docker.com:80"} {
req, _ := http.NewRequest(http.MethodGet, url, nil)
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], nil)
reqFrom.Header.Add("Content-Type", "application/json")
reqFrom.Header.Add("Authorization", "super_secret")
reqTo, _ := http.NewRequest(http.MethodGet, urls[1], nil)
_ = addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom})
if len(reqTo.Header) != 1 {
t.Fatalf("Expected 1 headers, got %d", len(reqTo.Header))
}
if reqTo.Header.Get("Content-Type") != "application/json" {
t.Fatal("'Content-Type' should be 'application/json'")
}
if reqTo.Header.Get("Authorization") != "" {
t.Fatal("'Authorization' should be empty")
}
}
for _, urls := range [][]string{
{"https://docker.io", "https://docker.com"},
{"https://foo.docker.io:7777", "https://bar.docker.com"},
} {
reqFrom, _ := http.NewRequest(http.MethodGet, urls[0], nil)
reqFrom.Header.Add("Content-Type", "application/json")
reqFrom.Header.Add("Authorization", "super_secret")
reqTo, _ := http.NewRequest(http.MethodGet, urls[1], nil)
_ = addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom})
if len(reqTo.Header) != 2 {
t.Fatalf("Expected 2 headers, got %d", len(reqTo.Header))
}
if reqTo.Header.Get("Content-Type") != "application/json" {
t.Fatal("'Content-Type' should be 'application/json'")
}
if reqTo.Header.Get("Authorization") != "super_secret" {
t.Fatal("'Authorization' should be 'super_secret'")
}
}
}
func TestAllowNondistributableArtifacts(t *testing.T) {
tests := []struct {
addr string
@ -707,26 +504,3 @@ func TestIsSecureIndex(t *testing.T) {
}
}
}
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
}

View file

@ -1,4 +1,4 @@
package registry // import "github.com/docker/docker/registry"
package registry
import (
"context"
@ -6,12 +6,11 @@ import (
"strconv"
"strings"
"github.com/containerd/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/docker/docker/errdefs"
"github.com/containerd/containerd/log"
"github.com/docker/distribution/registry/client/auth"
"github.com/pkg/errors"
)
@ -139,3 +138,26 @@ func (s *Service) searchUnfiltered(ctx context.Context, term string, limit int,
return newSession(client, endpoint).searchRepositories(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)
return newIndexInfo(emptyServiceConfig, indexName)
}

View file

@ -175,3 +175,48 @@ func (e *v1Endpoint) ping() (v1PingResult, error) {
log.G(context.TODO()).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(transport http.RoundTripper) *http.Client {
return &http.Client{
Transport: transport,
CheckRedirect: addRequiredHeadersToRedirectedRequests,
}
}
func trustedLocation(req *http.Request) bool {
var (
trusteds = []string{"docker.com", "docker.io"}
hostname = strings.SplitN(req.Host, ":", 2)[0]
)
if req.URL.Scheme != "https" {
return false
}
for _, trusted := range trusteds {
if hostname == trusted || strings.HasSuffix(hostname, "."+trusted) {
return true
}
}
return false
}
// addRequiredHeadersToRedirectedRequests adds the necessary redirection headers
// for redirected requests
func addRequiredHeadersToRedirectedRequests(req *http.Request, via []*http.Request) error {
if 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
}

View file

@ -0,0 +1,251 @@
package registry // import "github.com/docker/docker/registry"
import (
"net/http"
"net/http/httptest"
"net/url"
"os"
"strings"
"testing"
"github.com/docker/docker/api/types/registry"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
"gotest.tools/v3/skip"
)
func TestV1EndpointPing(t *testing.T) {
skip.If(t, os.Getuid() != 0, "skipping test that requires root")
testPing := func(index *registry.IndexInfo, expectedStandalone bool, assertMessage string) {
ep, err := newV1Endpoint(index, nil)
if err != nil {
t.Fatal(err)
}
regInfo, err := ep.ping()
if err != nil {
t.Fatal(err)
}
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) {
skip.If(t, os.Getuid() != 0, "skipping test that requires root")
// Simple wrapper to fail test if err != nil
expandEndpoint := func(index *registry.IndexInfo) *v1Endpoint {
endpoint, err := newV1Endpoint(index, nil)
if err != nil {
t.Fatal(err)
}
return endpoint
}
assertInsecureIndex := func(index *registry.IndexInfo) {
index.Secure = true
_, err := newV1Endpoint(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(index, nil)
assert.ErrorContains(t, err, "certificate signed by unknown authority", index.Name+": Expected cert error for secure index")
index.Secure = false
}
index := &registry.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(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 {
tc := tc
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()
testServerURL, err := url.Parse(testServer.URL)
if err != nil {
t.Fatal(err)
}
testEndpoint := v1Endpoint{
URL: testServerURL,
client: httpClient(newTransport(nil)),
}
if err = validateEndpoint(&testEndpoint); err != nil {
t.Fatal(err)
}
if testEndpoint.URL.Scheme != "http" {
t.Fatalf("expecting to validate endpoint as http, got url %s", testEndpoint.String())
}
}
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, nil)
assert.Check(t, !trustedLocation(req))
}
for _, u := range []string{"https://docker.io", "https://test.docker.com:80"} {
req, _ := http.NewRequest(http.MethodGet, u, nil)
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], nil)
reqFrom.Header.Add("Content-Type", "application/json")
reqFrom.Header.Add("Authorization", "super_secret")
reqTo, _ := http.NewRequest(http.MethodGet, urls[1], nil)
_ = addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom})
if len(reqTo.Header) != 1 {
t.Fatalf("Expected 1 headers, got %d", len(reqTo.Header))
}
if reqTo.Header.Get("Content-Type") != "application/json" {
t.Fatal("'Content-Type' should be 'application/json'")
}
if reqTo.Header.Get("Authorization") != "" {
t.Fatal("'Authorization' should be empty")
}
}
for _, urls := range [][]string{
{"https://docker.io", "https://docker.com"},
{"https://foo.docker.io:7777", "https://bar.docker.com"},
} {
reqFrom, _ := http.NewRequest(http.MethodGet, urls[0], nil)
reqFrom.Header.Add("Content-Type", "application/json")
reqFrom.Header.Add("Authorization", "super_secret")
reqTo, _ := http.NewRequest(http.MethodGet, urls[1], nil)
_ = addRequiredHeadersToRedirectedRequests(reqTo, []*http.Request{reqFrom})
if len(reqTo.Header) != 2 {
t.Fatalf("Expected 2 headers, got %d", len(reqTo.Header))
}
if reqTo.Header.Get("Content-Type") != "application/json" {
t.Fatal("'Content-Type' should be 'application/json'")
}
if reqTo.Header.Get("Authorization") != "super_secret" {
t.Fatal("'Authorization' should be 'super_secret'")
}
}
}

View file

@ -1,18 +1,87 @@
package registry // import "github.com/docker/docker/registry"
package registry
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"net/http/httputil"
"testing"
"github.com/docker/distribution/registry/client/transport"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/api/types/registry"
"github.com/docker/docker/errdefs"
"gotest.tools/v3/assert"
)
func spawnTestRegistrySession(t *testing.T) *session {
authConfig := &registry.AuthConfig{}
endpoint, err := newV1Endpoint(makeIndex("/v1/"), nil)
if err != nil {
t.Fatal(err)
}
userAgent := "docker test client"
var tr http.RoundTripper = debugTransport{newTransport(nil), t.Log}
tr = transport.NewTransport(newAuthTransport(tr, authConfig, false), Headers(userAgent, nil)...)
client := httpClient(tr)
if err := authorizeClient(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("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

View file

@ -91,18 +91,6 @@ func (s *Service) Auth(ctx context.Context, authConfig *registry.AuthConfig, use
return "", "", err
}
// splitReposSearchTerm breaks a search term into an index name and remote name
func splitReposSearchTerm(reposName string) (string, string) {
nameParts := strings.SplitN(reposName, "/", 2)
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]
}
// ResolveRepository splits a repository name into its components
// and configuration of the associated registry.
func (s *Service) ResolveRepository(name reference.Named) (*RepositoryInfo, error) {