Ver Fonte

Add `--limit` option to `docker search`

This fix tries to address the issue raised in #23055.
Currently `docker search` result caps at 25 and there is
no way to allow getting more results (if exist).

This fix adds the flag `--limit` so that it is possible
to return more results from the `docker search`.

Related documentation has been updated.

Additional tests have been added to cover the changes.

This fix fixes #23055.

Signed-off-by: Yong Tang <yong.tang.github@outlook.com>
Yong Tang há 9 anos atrás
pai
commit
92f10fe228

+ 2 - 0
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)
 	cmd := Cli.Subcmd("search", []string{"TERM"}, Cli.DockerCommands["search"].Description, true)
 	noTrunc := cmd.Bool([]string{"-no-trunc"}, false, "Don't truncate output")
 	noTrunc := cmd.Bool([]string{"-no-trunc"}, false, "Don't truncate output")
 	cmd.Var(&flFilter, []string{"f", "-filter"}, "Filter output based on conditions provided")
 	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"
 	// Deprecated since Docker 1.12 in favor of "--filter"
 	automated := cmd.Bool([]string{"#-automated"}, false, "Only show automated builds - DEPRECATED")
 	automated := cmd.Bool([]string{"#-automated"}, false, "Only show automated builds - DEPRECATED")
@@ -72,6 +73,7 @@ func (cli *DockerCli) CmdSearch(args ...string) error {
 		RegistryAuth:  encodedAuth,
 		RegistryAuth:  encodedAuth,
 		PrivilegeFunc: requestPrivilege,
 		PrivilegeFunc: requestPrivilege,
 		Filters:       filterArgs,
 		Filters:       filterArgs,
+		Limit:         *flLimit,
 	}
 	}
 
 
 	unorderedResults, err := cli.client.ImageSearch(ctx, name, options)
 	unorderedResults, err := cli.client.ImageSearch(ctx, name, options)

+ 1 - 1
api/server/router/image/backend.go

@@ -39,5 +39,5 @@ type importExportBackend interface {
 type registryBackend interface {
 type registryBackend interface {
 	PullImage(ctx context.Context, image, tag string, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error
 	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
 	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)
 }
 }

+ 11 - 1
api/server/router/image/image_routes.go

@@ -6,12 +6,14 @@ import (
 	"fmt"
 	"fmt"
 	"io"
 	"io"
 	"net/http"
 	"net/http"
+	"strconv"
 	"strings"
 	"strings"
 
 
 	"github.com/docker/docker/api/server/httputils"
 	"github.com/docker/docker/api/server/httputils"
 	"github.com/docker/docker/api/types/backend"
 	"github.com/docker/docker/api/types/backend"
 	"github.com/docker/docker/pkg/ioutils"
 	"github.com/docker/docker/pkg/ioutils"
 	"github.com/docker/docker/pkg/streamformatter"
 	"github.com/docker/docker/pkg/streamformatter"
+	"github.com/docker/docker/registry"
 	"github.com/docker/engine-api/types"
 	"github.com/docker/engine-api/types"
 	"github.com/docker/engine-api/types/container"
 	"github.com/docker/engine-api/types/container"
 	"github.com/docker/engine-api/types/versions"
 	"github.com/docker/engine-api/types/versions"
@@ -301,7 +303,15 @@ func (s *imageRouter) getImagesSearch(ctx context.Context, w http.ResponseWriter
 			headers[k] = v
 			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 {
 	if err != nil {
 		return err
 		return err
 	}
 	}

+ 2 - 2
daemon/search.go

@@ -20,7 +20,7 @@ var acceptedSearchFilterTags = map[string]bool{
 
 
 // SearchRegistryForImages queries the registry for images matching
 // SearchRegistryForImages queries the registry for images matching
 // term. authConfig is used to login.
 // 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,
 	authConfig *types.AuthConfig,
 	headers map[string][]string) (*registrytypes.SearchResults, error) {
 	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 {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}

+ 3 - 3
daemon/search_test.go

@@ -21,7 +21,7 @@ type FakeService struct {
 	results []registrytypes.SearchResult
 	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 {
 	if s.shouldReturnError {
 		return nil, fmt.Errorf("Search unknown error")
 		return nil, fmt.Errorf("Search unknown error")
 	}
 	}
@@ -81,7 +81,7 @@ func TestSearchRegistryForImagesErrors(t *testing.T) {
 				shouldReturnError: e.shouldReturnError,
 				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 {
 		if err == nil {
 			t.Errorf("%d: expected an error, got nothing", index)
 			t.Errorf("%d: expected an error, got nothing", index)
 		}
 		}
@@ -328,7 +328,7 @@ func TestSearchRegistryForImages(t *testing.T) {
 				results: s.registryResults,
 				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 {
 		if err != nil {
 			t.Errorf("%d: %v", index, err)
 			t.Errorf("%d: %v", index, err)
 		}
 		}

+ 1 - 0
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`.
 * `GET /images/json` now supports filters `since` and `before`.
 * `POST /containers/(id or name)/start` no longer accepts a `HostConfig`.
 * `POST /containers/(id or name)/start` no longer accepts a `HostConfig`.
 * `POST /images/(name)/tag` no longer has a `force` query parameter.
 * `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
 ### v1.23 API changes
 
 

+ 1 - 0
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:
 Query Parameters:
 
 
 -   **term** – term to search
 -   **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:
 -   **filters** – a JSON encoded value of the filters (a map[string][]string) to process on the images list. Available filters:
   -   `stars=<number>`
   -   `stars=<number>`
   -   `is-automated=(true|false)`
   -   `is-automated=(true|false)`

+ 7 - 0
docs/reference/commandline/search.md

@@ -19,6 +19,7 @@ parent = "smn_cli"
                            - is-official=(true|false)
                            - is-official=(true|false)
                            - stars=<number> - image has at least 'number' stars
                            - stars=<number> - image has at least 'number' stars
       --help               Print usage
       --help               Print usage
+      --limit=25           Maximum returned search results
       --no-trunc           Don't truncate output
       --no-trunc           Don't truncate output
 
 
 Search [Docker Hub](https://hub.docker.com) for images
 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]
     progrium/busybox                                                                                               50                   [OK]
     radial/busyboxplus   Full-chain, Internet enabled, busybox made from scratch. Comes in git and cURL flavors.   8                    [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
 ## Filtering
 
 
 The filtering flag (`-f` or `--filter`) format is a `key=value` pair. If there is more
 The filtering flag (`-f` or `--filter`) format is a `key=value` pair. If there is more

+ 32 - 0
integration-cli/docker_cli_search_test.go

@@ -1,6 +1,7 @@
 package main
 package main
 
 
 import (
 import (
+	"fmt"
 	"strings"
 	"strings"
 
 
 	"github.com/docker/docker/pkg/integration/checker"
 	"github.com/docker/docker/pkg/integration/checker"
@@ -97,3 +98,34 @@ func (s *DockerSuite) TestSearchOnCentralRegistryWithDash(c *check.C) {
 
 
 	dockerCmd(c, "search", "ubuntu-")
 	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))
+}

+ 4 - 0
man/docker-search.1.md

@@ -8,6 +8,7 @@ docker-search - Search the Docker Hub for images
 **docker search**
 **docker search**
 [**-f**|**--filter**[=*[]*]]
 [**-f**|**--filter**[=*[]*]]
 [**--help**]
 [**--help**]
+[**--limit**[=*LIMIT*]]
 [**--no-trunc**]
 [**--no-trunc**]
 TERM
 TERM
 
 
@@ -30,6 +31,9 @@ of stars awarded, whether the image is official, and whether it is automated.
 **--help**
 **--help**
   Print usage statement
   Print usage statement
 
 
+**--limit**=*LIMIT*
+  Maximum returned search results. The default is 25.
+
 **--no-trunc**=*true*|*false*
 **--no-trunc**=*true*|*false*
    Don't truncate output. The default is *false*.
    Don't truncate output. The default is *false*.
 
 

+ 1 - 1
registry/registry_test.go

@@ -730,7 +730,7 @@ func TestPushImageJSONIndex(t *testing.T) {
 
 
 func TestSearchRepositories(t *testing.T) {
 func TestSearchRepositories(t *testing.T) {
 	r := spawnTestRegistrySession(t)
 	r := spawnTestRegistrySession(t)
-	results, err := r.SearchRepositories("fakequery")
+	results, err := r.SearchRepositories("fakequery", 25)
 	if err != nil {
 	if err != nil {
 		t.Fatal(err)
 		t.Fatal(err)
 	}
 	}

+ 9 - 4
registry/service.go

@@ -15,6 +15,11 @@ import (
 	registrytypes "github.com/docker/engine-api/types/registry"
 	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.
 // Service is the interface defining what a registry service should implement.
 type Service interface {
 type Service interface {
 	Auth(ctx context.Context, authConfig *types.AuthConfig, userAgent string) (status, token string, err error)
 	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)
 	LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error)
 	ResolveRepository(name reference.Named) (*RepositoryInfo, error)
 	ResolveRepository(name reference.Named) (*RepositoryInfo, error)
 	ResolveIndex(name string) (*registrytypes.IndexInfo, 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
 	ServiceConfig() *registrytypes.ServiceConfig
 	TLSConfig(hostname string) (*tls.Config, error)
 	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 queries the public registry for images matching the specified
 // search terms, and returns the results.
 // 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
 	// TODO Use ctx when searching for repositories
 	if err := validateNoScheme(term); err != nil {
 	if err := validateNoScheme(term); err != nil {
 		return nil, err
 		return nil, err
@@ -139,9 +144,9 @@ func (s *DefaultService) Search(ctx context.Context, term string, authConfig *ty
 			localName = strings.SplitN(localName, "/", 2)[1]
 			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
 // ResolveRepository splits a repository name into its components

+ 5 - 2
registry/session.go

@@ -721,9 +721,12 @@ func shouldRedirect(response *http.Response) bool {
 }
 }
 
 
 // SearchRepositories performs a search against the remote repository
 // 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)
 	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)
 	req, err := http.NewRequest("GET", u, nil)
 	if err != nil {
 	if err != nil {