mirror of
https://github.com/moby/moby.git
synced 2022-11-09 12:21:53 -05:00
Merge pull request #23107 from yongtang/23055-docker-search-limit
Add `--limit` option to `docker search`
This commit is contained in:
commit
020a86b3d9
13 changed files with 79 additions and 14 deletions
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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)`
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
|
@ -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*.
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue