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>
| ... | ... |
@@ -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 {
|