diff --git a/api/client/search.go b/api/client/search.go index d00d5a3d01..d2d1446239 100644 --- a/api/client/search.go +++ b/api/client/search.go @@ -34,6 +34,7 @@ func (cli *DockerCli) CmdSearch(args ...string) error { cmd := Cli.Subcmd("search", []string{"TERM"}, Cli.DockerCommands["search"].Description, true) noTrunc := cmd.Bool([]string{"-no-trunc"}, false, "Don't truncate output") cmd.Var(&flFilter, []string{"f", "-filter"}, "Filter output based on conditions provided") + flLimit := cmd.Int([]string{"-limit"}, registry.DefaultSearchLimit, "Max number of search results") // Deprecated since Docker 1.12 in favor of "--filter" automated := cmd.Bool([]string{"#-automated"}, false, "Only show automated builds - DEPRECATED") @@ -72,6 +73,7 @@ func (cli *DockerCli) CmdSearch(args ...string) error { RegistryAuth: encodedAuth, PrivilegeFunc: requestPrivilege, Filters: filterArgs, + Limit: *flLimit, } unorderedResults, err := cli.client.ImageSearch(ctx, name, options) diff --git a/api/server/router/image/backend.go b/api/server/router/image/backend.go index cc854a2768..08101736e0 100644 --- a/api/server/router/image/backend.go +++ b/api/server/router/image/backend.go @@ -39,5 +39,5 @@ type importExportBackend interface { type registryBackend interface { PullImage(ctx context.Context, image, tag string, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error PushImage(ctx context.Context, image, tag string, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error - SearchRegistryForImages(ctx context.Context, filtersArgs string, term string, authConfig *types.AuthConfig, metaHeaders map[string][]string) (*registry.SearchResults, error) + SearchRegistryForImages(ctx context.Context, filtersArgs string, term string, limit int, authConfig *types.AuthConfig, metaHeaders map[string][]string) (*registry.SearchResults, error) } diff --git a/api/server/router/image/image_routes.go b/api/server/router/image/image_routes.go index 3b8180f214..18a36fda6d 100644 --- a/api/server/router/image/image_routes.go +++ b/api/server/router/image/image_routes.go @@ -6,12 +6,14 @@ import ( "fmt" "io" "net/http" + "strconv" "strings" "github.com/docker/docker/api/server/httputils" "github.com/docker/docker/api/types/backend" "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/streamformatter" + "github.com/docker/docker/registry" "github.com/docker/engine-api/types" "github.com/docker/engine-api/types/container" "github.com/docker/engine-api/types/versions" @@ -301,7 +303,15 @@ func (s *imageRouter) getImagesSearch(ctx context.Context, w http.ResponseWriter headers[k] = v } } - query, err := s.backend.SearchRegistryForImages(ctx, r.Form.Get("filters"), r.Form.Get("term"), config, headers) + limit := registry.DefaultSearchLimit + if r.Form.Get("limit") != "" { + limitValue, err := strconv.Atoi(r.Form.Get("limit")) + if err != nil { + return err + } + limit = limitValue + } + query, err := s.backend.SearchRegistryForImages(ctx, r.Form.Get("filters"), r.Form.Get("term"), limit, config, headers) if err != nil { return err } diff --git a/daemon/search.go b/daemon/search.go index 4f749608f8..09c6ae49b7 100644 --- a/daemon/search.go +++ b/daemon/search.go @@ -20,7 +20,7 @@ var acceptedSearchFilterTags = map[string]bool{ // SearchRegistryForImages queries the registry for images matching // term. authConfig is used to login. -func (daemon *Daemon) SearchRegistryForImages(ctx context.Context, filtersArgs string, term string, +func (daemon *Daemon) SearchRegistryForImages(ctx context.Context, filtersArgs string, term string, limit int, authConfig *types.AuthConfig, headers map[string][]string) (*registrytypes.SearchResults, error) { @@ -61,7 +61,7 @@ func (daemon *Daemon) SearchRegistryForImages(ctx context.Context, filtersArgs s } } - unfilteredResult, err := daemon.RegistryService.Search(ctx, term, authConfig, dockerversion.DockerUserAgent(ctx), headers) + unfilteredResult, err := daemon.RegistryService.Search(ctx, term, limit, authConfig, dockerversion.DockerUserAgent(ctx), headers) if err != nil { return nil, err } diff --git a/daemon/search_test.go b/daemon/search_test.go index b3dcb49d3d..bffa84e192 100644 --- a/daemon/search_test.go +++ b/daemon/search_test.go @@ -21,7 +21,7 @@ type FakeService struct { results []registrytypes.SearchResult } -func (s *FakeService) Search(ctx context.Context, term string, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) { +func (s *FakeService) Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) { if s.shouldReturnError { return nil, fmt.Errorf("Search unknown error") } @@ -81,7 +81,7 @@ func TestSearchRegistryForImagesErrors(t *testing.T) { shouldReturnError: e.shouldReturnError, }, } - _, err := daemon.SearchRegistryForImages(context.Background(), e.filtersArgs, "term", nil, map[string][]string{}) + _, err := daemon.SearchRegistryForImages(context.Background(), e.filtersArgs, "term", 25, nil, map[string][]string{}) if err == nil { t.Errorf("%d: expected an error, got nothing", index) } @@ -328,7 +328,7 @@ func TestSearchRegistryForImages(t *testing.T) { results: s.registryResults, }, } - results, err := daemon.SearchRegistryForImages(context.Background(), s.filtersArgs, term, nil, map[string][]string{}) + results, err := daemon.SearchRegistryForImages(context.Background(), s.filtersArgs, term, 25, nil, map[string][]string{}) if err != nil { t.Errorf("%d: %v", index, err) } diff --git a/docs/reference/api/docker_remote_api.md b/docs/reference/api/docker_remote_api.md index 87760b3fc6..58c0743959 100644 --- a/docs/reference/api/docker_remote_api.md +++ b/docs/reference/api/docker_remote_api.md @@ -124,6 +124,7 @@ This section lists each version from latest to oldest. Each listing includes a * `GET /images/json` now supports filters `since` and `before`. * `POST /containers/(id or name)/start` no longer accepts a `HostConfig`. * `POST /images/(name)/tag` no longer has a `force` query parameter. +* `GET /images/search` now supports maximum returned search results `limit`. ### v1.23 API changes diff --git a/docs/reference/api/docker_remote_api_v1.24.md b/docs/reference/api/docker_remote_api_v1.24.md index 94265abb0e..5fd040389a 100644 --- a/docs/reference/api/docker_remote_api_v1.24.md +++ b/docs/reference/api/docker_remote_api_v1.24.md @@ -2130,6 +2130,7 @@ Search for an image on [Docker Hub](https://hub.docker.com). Query Parameters: - **term** – term to search +- **limit** – maximum returned search results - **filters** – a JSON encoded value of the filters (a map[string][]string) to process on the images list. Available filters: - `stars=` - `is-automated=(true|false)` diff --git a/docs/reference/commandline/search.md b/docs/reference/commandline/search.md index 8bca98f0c9..6d3984de13 100644 --- a/docs/reference/commandline/search.md +++ b/docs/reference/commandline/search.md @@ -19,6 +19,7 @@ parent = "smn_cli" - is-official=(true|false) - stars= - image has at least 'number' stars --help Print usage + --limit=25 Maximum returned search results --no-trunc Don't truncate output Search [Docker Hub](https://hub.docker.com) for images @@ -74,6 +75,12 @@ at least 3 stars and the description isn't truncated in the output: progrium/busybox 50 [OK] radial/busyboxplus Full-chain, Internet enabled, busybox made from scratch. Comes in git and cURL flavors. 8 [OK] +## Limit search results (--limit) + +The flag `--limit` is the maximium number of results returned by a search. This value could +be in the range between 1 and 100. The default value of `--limit` is 25. + + ## Filtering The filtering flag (`-f` or `--filter`) format is a `key=value` pair. If there is more diff --git a/integration-cli/docker_cli_search_test.go b/integration-cli/docker_cli_search_test.go index a93d657265..ba8d0021a7 100644 --- a/integration-cli/docker_cli_search_test.go +++ b/integration-cli/docker_cli_search_test.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "strings" "github.com/docker/docker/pkg/integration/checker" @@ -97,3 +98,34 @@ func (s *DockerSuite) TestSearchOnCentralRegistryWithDash(c *check.C) { dockerCmd(c, "search", "ubuntu-") } + +// test case for #23055 +func (s *DockerSuite) TestSearchWithLimit(c *check.C) { + testRequires(c, Network, DaemonIsLinux) + + limit := 10 + out, _, err := dockerCmdWithError("search", fmt.Sprintf("--limit=%d", limit), "docker") + c.Assert(err, checker.IsNil) + outSlice := strings.Split(out, "\n") + c.Assert(outSlice, checker.HasLen, limit+2) // 1 header, 1 carriage return + + limit = 50 + out, _, err = dockerCmdWithError("search", fmt.Sprintf("--limit=%d", limit), "docker") + c.Assert(err, checker.IsNil) + outSlice = strings.Split(out, "\n") + c.Assert(outSlice, checker.HasLen, limit+2) // 1 header, 1 carriage return + + limit = 100 + out, _, err = dockerCmdWithError("search", fmt.Sprintf("--limit=%d", limit), "docker") + c.Assert(err, checker.IsNil) + outSlice = strings.Split(out, "\n") + c.Assert(outSlice, checker.HasLen, limit+2) // 1 header, 1 carriage return + + limit = 0 + out, _, err = dockerCmdWithError("search", fmt.Sprintf("--limit=%d", limit), "docker") + c.Assert(err, checker.Not(checker.IsNil)) + + limit = 200 + out, _, err = dockerCmdWithError("search", fmt.Sprintf("--limit=%d", limit), "docker") + c.Assert(err, checker.Not(checker.IsNil)) +} diff --git a/man/docker-search.1.md b/man/docker-search.1.md index c1728f548c..ad8bbc78b2 100644 --- a/man/docker-search.1.md +++ b/man/docker-search.1.md @@ -8,6 +8,7 @@ docker-search - Search the Docker Hub for images **docker search** [**-f**|**--filter**[=*[]*]] [**--help**] +[**--limit**[=*LIMIT*]] [**--no-trunc**] TERM @@ -30,6 +31,9 @@ of stars awarded, whether the image is official, and whether it is automated. **--help** Print usage statement +**--limit**=*LIMIT* + Maximum returned search results. The default is 25. + **--no-trunc**=*true*|*false* Don't truncate output. The default is *false*. diff --git a/registry/registry_test.go b/registry/registry_test.go index 39a01bcd4a..9927af32d8 100644 --- a/registry/registry_test.go +++ b/registry/registry_test.go @@ -730,7 +730,7 @@ func TestPushImageJSONIndex(t *testing.T) { func TestSearchRepositories(t *testing.T) { r := spawnTestRegistrySession(t) - results, err := r.SearchRepositories("fakequery") + results, err := r.SearchRepositories("fakequery", 25) if err != nil { t.Fatal(err) } diff --git a/registry/service.go b/registry/service.go index d48063cd78..25b4990e80 100644 --- a/registry/service.go +++ b/registry/service.go @@ -15,6 +15,11 @@ import ( registrytypes "github.com/docker/engine-api/types/registry" ) +const ( + // DefaultSearchLimit is the default value for maximum number of returned search results. + DefaultSearchLimit = 25 +) + // Service is the interface defining what a registry service should implement. type Service interface { Auth(ctx context.Context, authConfig *types.AuthConfig, userAgent string) (status, token string, err error) @@ -22,7 +27,7 @@ type Service interface { LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error) ResolveRepository(name reference.Named) (*RepositoryInfo, error) ResolveIndex(name string) (*registrytypes.IndexInfo, error) - Search(ctx context.Context, term string, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) + Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) ServiceConfig() *registrytypes.ServiceConfig TLSConfig(hostname string) (*tls.Config, error) } @@ -108,7 +113,7 @@ func splitReposSearchTerm(reposName string) (string, string) { // Search queries the public registry for images matching the specified // search terms, and returns the results. -func (s *DefaultService) Search(ctx context.Context, term string, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) { +func (s *DefaultService) Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) { // TODO Use ctx when searching for repositories if err := validateNoScheme(term); err != nil { return nil, err @@ -139,9 +144,9 @@ func (s *DefaultService) Search(ctx context.Context, term string, authConfig *ty localName = strings.SplitN(localName, "/", 2)[1] } - return r.SearchRepositories(localName) + return r.SearchRepositories(localName, limit) } - return r.SearchRepositories(remoteName) + return r.SearchRepositories(remoteName, limit) } // ResolveRepository splits a repository name into its components diff --git a/registry/session.go b/registry/session.go index 82593cd7eb..140c458eb9 100644 --- a/registry/session.go +++ b/registry/session.go @@ -721,9 +721,12 @@ func shouldRedirect(response *http.Response) bool { } // SearchRepositories performs a search against the remote repository -func (r *Session) SearchRepositories(term string) (*registrytypes.SearchResults, error) { +func (r *Session) SearchRepositories(term string, limit int) (*registrytypes.SearchResults, error) { + if limit < 1 || limit > 100 { + return nil, fmt.Errorf("Limit %d is outside the range of [1, 100]", limit) + } logrus.Debugf("Index server: %s", r.indexEndpoint) - u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term) + u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term) + "&n=" + url.QueryEscape(fmt.Sprintf("%d", limit)) req, err := http.NewRequest("GET", u, nil) if err != nil {