3991faf464
SearchRegistryForImages does not make sense as part of the image service interface. The implementation just wraps the search API of the registry service to filter the results client-side. It has nothing to do with local image storage, and the implementation of search does not need to change when changing which backend (graph driver vs. containerd snapshotter) is used for local image storage. Filtering of the search results is an implementation detail: the consumer of the results does not care which actor does the filtering so long as the results are filtered as requested. Move filtering into the exported API of the registry service to hide the implementation details. Only one thing---the registry service implementation---would need to change in order to support server-side filtering of search results if Docker Hub or other registry servers were to add support for it to their APIs. Use a fake registry server in the search unit tests to avoid having to mock out the registry API client. Signed-off-by: Cory Snider <csnider@mirantis.com>
361 lines
8.8 KiB
Go
361 lines
8.8 KiB
Go
package registry // import "github.com/docker/docker/registry"
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/docker/docker/api/types/filters"
|
|
"github.com/docker/docker/api/types/registry"
|
|
"github.com/docker/docker/errdefs"
|
|
"gotest.tools/v3/assert"
|
|
)
|
|
|
|
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 {
|
|
tc := tc
|
|
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, errdefs.IsUnknown(err), "got: %T: %v", err, err)
|
|
return
|
|
}
|
|
assert.Check(t, errdefs.IsInvalidParameter(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,
|
|
},
|
|
},
|
|
expectedResults: []registry.SearchResult{
|
|
{
|
|
Name: "name",
|
|
Description: "description",
|
|
IsAutomated: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "is-automated=false, no results",
|
|
filtersArgs: filters.NewArgs(filters.Arg("is-automated", "false")),
|
|
registryResults: []registry.SearchResult{
|
|
{
|
|
Name: "name",
|
|
Description: "description",
|
|
IsAutomated: true,
|
|
},
|
|
},
|
|
expectedResults: []registry.SearchResult{},
|
|
},
|
|
{
|
|
name: "is-automated=false",
|
|
filtersArgs: filters.NewArgs(filters.Arg("is-automated", "false")),
|
|
registryResults: []registry.SearchResult{
|
|
{
|
|
Name: "name",
|
|
Description: "description",
|
|
IsAutomated: false,
|
|
},
|
|
},
|
|
expectedResults: []registry.SearchResult{
|
|
{
|
|
Name: "name",
|
|
Description: "description",
|
|
IsAutomated: false,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
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,
|
|
},
|
|
{
|
|
Name: "name1",
|
|
Description: "description1",
|
|
StarCount: 1,
|
|
IsOfficial: false,
|
|
IsAutomated: true,
|
|
},
|
|
{
|
|
Name: "name2",
|
|
Description: "description2",
|
|
StarCount: 1,
|
|
IsOfficial: true,
|
|
IsAutomated: false,
|
|
},
|
|
{
|
|
Name: "name3",
|
|
Description: "description3",
|
|
StarCount: 2,
|
|
IsOfficial: true,
|
|
IsAutomated: true,
|
|
},
|
|
},
|
|
expectedResults: []registry.SearchResult{
|
|
{
|
|
Name: "name3",
|
|
Description: "description3",
|
|
StarCount: 2,
|
|
IsOfficial: true,
|
|
IsAutomated: true,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
for _, tc := range successCases {
|
|
tc := tc
|
|
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)
|
|
})
|
|
}
|
|
}
|