Browse code

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 authored on 2016/06/02 05:38:14
Showing 13 changed files
... ...
@@ -34,6 +34,7 @@ func (cli *DockerCli) CmdSearch(args ...string) error {
34 34
 	cmd := Cli.Subcmd("search", []string{"TERM"}, Cli.DockerCommands["search"].Description, true)
35 35
 	noTrunc := cmd.Bool([]string{"-no-trunc"}, false, "Don't truncate output")
36 36
 	cmd.Var(&flFilter, []string{"f", "-filter"}, "Filter output based on conditions provided")
37
+	flLimit := cmd.Int([]string{"-limit"}, registry.DefaultSearchLimit, "Max number of search results")
37 38
 
38 39
 	// Deprecated since Docker 1.12 in favor of "--filter"
39 40
 	automated := cmd.Bool([]string{"#-automated"}, false, "Only show automated builds - DEPRECATED")
... ...
@@ -72,6 +73,7 @@ func (cli *DockerCli) CmdSearch(args ...string) error {
72 72
 		RegistryAuth:  encodedAuth,
73 73
 		PrivilegeFunc: requestPrivilege,
74 74
 		Filters:       filterArgs,
75
+		Limit:         *flLimit,
75 76
 	}
76 77
 
77 78
 	unorderedResults, err := cli.client.ImageSearch(ctx, name, options)
... ...
@@ -39,5 +39,5 @@ type importExportBackend interface {
39 39
 type registryBackend interface {
40 40
 	PullImage(ctx context.Context, image, tag string, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error
41 41
 	PushImage(ctx context.Context, image, tag string, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error
42
-	SearchRegistryForImages(ctx context.Context, filtersArgs string, term string, authConfig *types.AuthConfig, metaHeaders map[string][]string) (*registry.SearchResults, error)
42
+	SearchRegistryForImages(ctx context.Context, filtersArgs string, term string, limit int, authConfig *types.AuthConfig, metaHeaders map[string][]string) (*registry.SearchResults, error)
43 43
 }
... ...
@@ -6,12 +6,14 @@ import (
6 6
 	"fmt"
7 7
 	"io"
8 8
 	"net/http"
9
+	"strconv"
9 10
 	"strings"
10 11
 
11 12
 	"github.com/docker/docker/api/server/httputils"
12 13
 	"github.com/docker/docker/api/types/backend"
13 14
 	"github.com/docker/docker/pkg/ioutils"
14 15
 	"github.com/docker/docker/pkg/streamformatter"
16
+	"github.com/docker/docker/registry"
15 17
 	"github.com/docker/engine-api/types"
16 18
 	"github.com/docker/engine-api/types/container"
17 19
 	"github.com/docker/engine-api/types/versions"
... ...
@@ -301,7 +303,15 @@ func (s *imageRouter) getImagesSearch(ctx context.Context, w http.ResponseWriter
301 301
 			headers[k] = v
302 302
 		}
303 303
 	}
304
-	query, err := s.backend.SearchRegistryForImages(ctx, r.Form.Get("filters"), r.Form.Get("term"), config, headers)
304
+	limit := registry.DefaultSearchLimit
305
+	if r.Form.Get("limit") != "" {
306
+		limitValue, err := strconv.Atoi(r.Form.Get("limit"))
307
+		if err != nil {
308
+			return err
309
+		}
310
+		limit = limitValue
311
+	}
312
+	query, err := s.backend.SearchRegistryForImages(ctx, r.Form.Get("filters"), r.Form.Get("term"), limit, config, headers)
305 313
 	if err != nil {
306 314
 		return err
307 315
 	}
... ...
@@ -20,7 +20,7 @@ var acceptedSearchFilterTags = map[string]bool{
20 20
 
21 21
 // SearchRegistryForImages queries the registry for images matching
22 22
 // term. authConfig is used to login.
23
-func (daemon *Daemon) SearchRegistryForImages(ctx context.Context, filtersArgs string, term string,
23
+func (daemon *Daemon) SearchRegistryForImages(ctx context.Context, filtersArgs string, term string, limit int,
24 24
 	authConfig *types.AuthConfig,
25 25
 	headers map[string][]string) (*registrytypes.SearchResults, error) {
26 26
 
... ...
@@ -61,7 +61,7 @@ func (daemon *Daemon) SearchRegistryForImages(ctx context.Context, filtersArgs s
61 61
 		}
62 62
 	}
63 63
 
64
-	unfilteredResult, err := daemon.RegistryService.Search(ctx, term, authConfig, dockerversion.DockerUserAgent(ctx), headers)
64
+	unfilteredResult, err := daemon.RegistryService.Search(ctx, term, limit, authConfig, dockerversion.DockerUserAgent(ctx), headers)
65 65
 	if err != nil {
66 66
 		return nil, err
67 67
 	}
... ...
@@ -21,7 +21,7 @@ type FakeService struct {
21 21
 	results []registrytypes.SearchResult
22 22
 }
23 23
 
24
-func (s *FakeService) Search(ctx context.Context, term string, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) {
24
+func (s *FakeService) Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) {
25 25
 	if s.shouldReturnError {
26 26
 		return nil, fmt.Errorf("Search unknown error")
27 27
 	}
... ...
@@ -81,7 +81,7 @@ func TestSearchRegistryForImagesErrors(t *testing.T) {
81 81
 				shouldReturnError: e.shouldReturnError,
82 82
 			},
83 83
 		}
84
-		_, err := daemon.SearchRegistryForImages(context.Background(), e.filtersArgs, "term", nil, map[string][]string{})
84
+		_, err := daemon.SearchRegistryForImages(context.Background(), e.filtersArgs, "term", 25, nil, map[string][]string{})
85 85
 		if err == nil {
86 86
 			t.Errorf("%d: expected an error, got nothing", index)
87 87
 		}
... ...
@@ -328,7 +328,7 @@ func TestSearchRegistryForImages(t *testing.T) {
328 328
 				results: s.registryResults,
329 329
 			},
330 330
 		}
331
-		results, err := daemon.SearchRegistryForImages(context.Background(), s.filtersArgs, term, nil, map[string][]string{})
331
+		results, err := daemon.SearchRegistryForImages(context.Background(), s.filtersArgs, term, 25, nil, map[string][]string{})
332 332
 		if err != nil {
333 333
 			t.Errorf("%d: %v", index, err)
334 334
 		}
... ...
@@ -124,6 +124,7 @@ This section lists each version from latest to oldest.  Each listing includes a
124 124
 * `GET /images/json` now supports filters `since` and `before`.
125 125
 * `POST /containers/(id or name)/start` no longer accepts a `HostConfig`.
126 126
 * `POST /images/(name)/tag` no longer has a `force` query parameter.
127
+* `GET /images/search` now supports maximum returned search results `limit`.
127 128
 
128 129
 ### v1.23 API changes
129 130
 
... ...
@@ -2130,6 +2130,7 @@ Search for an image on [Docker Hub](https://hub.docker.com).
2130 2130
 Query Parameters:
2131 2131
 
2132 2132
 -   **term** – term to search
2133
+-   **limit** – maximum returned search results
2133 2134
 -   **filters** – a JSON encoded value of the filters (a map[string][]string) to process on the images list. Available filters:
2134 2135
   -   `stars=<number>`
2135 2136
   -   `is-automated=(true|false)`
... ...
@@ -19,6 +19,7 @@ parent = "smn_cli"
19 19
                            - is-official=(true|false)
20 20
                            - stars=<number> - image has at least 'number' stars
21 21
       --help               Print usage
22
+      --limit=25           Maximum returned search results
22 23
       --no-trunc           Don't truncate output
23 24
 
24 25
 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:
74 74
     progrium/busybox                                                                                               50                   [OK]
75 75
     radial/busyboxplus   Full-chain, Internet enabled, busybox made from scratch. Comes in git and cURL flavors.   8                    [OK]
76 76
 
77
+## Limit search results (--limit)
78
+
79
+The flag `--limit` is the maximium number of results returned by a search. This value could
80
+be in the range between 1 and 100. The default value of `--limit` is 25.
81
+
82
+
77 83
 ## Filtering
78 84
 
79 85
 The filtering flag (`-f` or `--filter`) format is a `key=value` pair. If there is more
... ...
@@ -1,6 +1,7 @@
1 1
 package main
2 2
 
3 3
 import (
4
+	"fmt"
4 5
 	"strings"
5 6
 
6 7
 	"github.com/docker/docker/pkg/integration/checker"
... ...
@@ -97,3 +98,34 @@ func (s *DockerSuite) TestSearchOnCentralRegistryWithDash(c *check.C) {
97 97
 
98 98
 	dockerCmd(c, "search", "ubuntu-")
99 99
 }
100
+
101
+// test case for #23055
102
+func (s *DockerSuite) TestSearchWithLimit(c *check.C) {
103
+	testRequires(c, Network, DaemonIsLinux)
104
+
105
+	limit := 10
106
+	out, _, err := dockerCmdWithError("search", fmt.Sprintf("--limit=%d", limit), "docker")
107
+	c.Assert(err, checker.IsNil)
108
+	outSlice := strings.Split(out, "\n")
109
+	c.Assert(outSlice, checker.HasLen, limit+2) // 1 header, 1 carriage return
110
+
111
+	limit = 50
112
+	out, _, err = dockerCmdWithError("search", fmt.Sprintf("--limit=%d", limit), "docker")
113
+	c.Assert(err, checker.IsNil)
114
+	outSlice = strings.Split(out, "\n")
115
+	c.Assert(outSlice, checker.HasLen, limit+2) // 1 header, 1 carriage return
116
+
117
+	limit = 100
118
+	out, _, err = dockerCmdWithError("search", fmt.Sprintf("--limit=%d", limit), "docker")
119
+	c.Assert(err, checker.IsNil)
120
+	outSlice = strings.Split(out, "\n")
121
+	c.Assert(outSlice, checker.HasLen, limit+2) // 1 header, 1 carriage return
122
+
123
+	limit = 0
124
+	out, _, err = dockerCmdWithError("search", fmt.Sprintf("--limit=%d", limit), "docker")
125
+	c.Assert(err, checker.Not(checker.IsNil))
126
+
127
+	limit = 200
128
+	out, _, err = dockerCmdWithError("search", fmt.Sprintf("--limit=%d", limit), "docker")
129
+	c.Assert(err, checker.Not(checker.IsNil))
130
+}
... ...
@@ -8,6 +8,7 @@ docker-search - Search the Docker Hub for images
8 8
 **docker search**
9 9
 [**-f**|**--filter**[=*[]*]]
10 10
 [**--help**]
11
+[**--limit**[=*LIMIT*]]
11 12
 [**--no-trunc**]
12 13
 TERM
13 14
 
... ...
@@ -30,6 +31,9 @@ of stars awarded, whether the image is official, and whether it is automated.
30 30
 **--help**
31 31
   Print usage statement
32 32
 
33
+**--limit**=*LIMIT*
34
+  Maximum returned search results. The default is 25.
35
+
33 36
 **--no-trunc**=*true*|*false*
34 37
    Don't truncate output. The default is *false*.
35 38
 
... ...
@@ -730,7 +730,7 @@ func TestPushImageJSONIndex(t *testing.T) {
730 730
 
731 731
 func TestSearchRepositories(t *testing.T) {
732 732
 	r := spawnTestRegistrySession(t)
733
-	results, err := r.SearchRepositories("fakequery")
733
+	results, err := r.SearchRepositories("fakequery", 25)
734 734
 	if err != nil {
735 735
 		t.Fatal(err)
736 736
 	}
... ...
@@ -15,6 +15,11 @@ import (
15 15
 	registrytypes "github.com/docker/engine-api/types/registry"
16 16
 )
17 17
 
18
+const (
19
+	// DefaultSearchLimit is the default value for maximum number of returned search results.
20
+	DefaultSearchLimit = 25
21
+)
22
+
18 23
 // Service is the interface defining what a registry service should implement.
19 24
 type Service interface {
20 25
 	Auth(ctx context.Context, authConfig *types.AuthConfig, userAgent string) (status, token string, err error)
... ...
@@ -22,7 +27,7 @@ type Service interface {
22 22
 	LookupPushEndpoints(hostname string) (endpoints []APIEndpoint, err error)
23 23
 	ResolveRepository(name reference.Named) (*RepositoryInfo, error)
24 24
 	ResolveIndex(name string) (*registrytypes.IndexInfo, error)
25
-	Search(ctx context.Context, term string, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error)
25
+	Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error)
26 26
 	ServiceConfig() *registrytypes.ServiceConfig
27 27
 	TLSConfig(hostname string) (*tls.Config, error)
28 28
 }
... ...
@@ -108,7 +113,7 @@ func splitReposSearchTerm(reposName string) (string, string) {
108 108
 
109 109
 // Search queries the public registry for images matching the specified
110 110
 // search terms, and returns the results.
111
-func (s *DefaultService) Search(ctx context.Context, term string, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) {
111
+func (s *DefaultService) Search(ctx context.Context, term string, limit int, authConfig *types.AuthConfig, userAgent string, headers map[string][]string) (*registrytypes.SearchResults, error) {
112 112
 	// TODO Use ctx when searching for repositories
113 113
 	if err := validateNoScheme(term); err != nil {
114 114
 		return nil, err
... ...
@@ -139,9 +144,9 @@ func (s *DefaultService) Search(ctx context.Context, term string, authConfig *ty
139 139
 			localName = strings.SplitN(localName, "/", 2)[1]
140 140
 		}
141 141
 
142
-		return r.SearchRepositories(localName)
142
+		return r.SearchRepositories(localName, limit)
143 143
 	}
144
-	return r.SearchRepositories(remoteName)
144
+	return r.SearchRepositories(remoteName, limit)
145 145
 }
146 146
 
147 147
 // ResolveRepository splits a repository name into its components
... ...
@@ -721,9 +721,12 @@ func shouldRedirect(response *http.Response) bool {
721 721
 }
722 722
 
723 723
 // SearchRepositories performs a search against the remote repository
724
-func (r *Session) SearchRepositories(term string) (*registrytypes.SearchResults, error) {
724
+func (r *Session) SearchRepositories(term string, limit int) (*registrytypes.SearchResults, error) {
725
+	if limit < 1 || limit > 100 {
726
+		return nil, fmt.Errorf("Limit %d is outside the range of [1, 100]", limit)
727
+	}
725 728
 	logrus.Debugf("Index server: %s", r.indexEndpoint)
726
-	u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term)
729
+	u := r.indexEndpoint.String() + "search?q=" + url.QueryEscape(term) + "&n=" + url.QueryEscape(fmt.Sprintf("%d", limit))
727 730
 
728 731
 	req, err := http.NewRequest("GET", u, nil)
729 732
 	if err != nil {