diff --git a/api/client/search.go b/api/client/search.go index 5ee6b8b652..b0b59b5148 100644 --- a/api/client/search.go +++ b/api/client/search.go @@ -10,10 +10,12 @@ import ( "golang.org/x/net/context" Cli "github.com/docker/docker/cli" + "github.com/docker/docker/opts" flag "github.com/docker/docker/pkg/mflag" "github.com/docker/docker/pkg/stringutils" "github.com/docker/docker/registry" "github.com/docker/engine-api/types" + "github.com/docker/engine-api/types/filters" registrytypes "github.com/docker/engine-api/types/registry" ) @@ -21,14 +23,32 @@ import ( // // Usage: docker search [OPTIONS] TERM func (cli *DockerCli) CmdSearch(args ...string) error { + var ( + err error + + filterArgs = filters.NewArgs() + + flFilter = opts.NewListOpts(nil) + ) + cmd := Cli.Subcmd("search", []string{"TERM"}, Cli.DockerCommands["search"].Description, true) noTrunc := cmd.Bool([]string{"-no-trunc"}, false, "Don't truncate output") - automated := cmd.Bool([]string{"-automated"}, false, "Only show automated builds") - stars := cmd.Uint([]string{"s", "-stars"}, 0, "Only displays with at least x stars") + cmd.Var(&flFilter, []string{"f", "-filter"}, "Filter output based on conditions provided") + + // Deprecated since Docker 1.12 in favor of "--filter" + automated := cmd.Bool([]string{"#-automated"}, false, "Only show automated builds - DEPRECATED") + stars := cmd.Uint([]string{"s", "#-stars"}, 0, "Only displays with at least x stars - DEPRECATED") + cmd.Require(flag.Exact, 1) cmd.ParseFlags(args, true) + for _, f := range flFilter.GetAll() { + if filterArgs, err = filters.ParseFlag(f, filterArgs); err != nil { + return err + } + } + name := cmd.Arg(0) v := url.Values{} v.Set("term", name) @@ -49,6 +69,7 @@ func (cli *DockerCli) CmdSearch(args ...string) error { options := types.ImageSearchOptions{ RegistryAuth: encodedAuth, PrivilegeFunc: requestPrivilege, + Filters: filterArgs, } unorderedResults, err := cli.client.ImageSearch(context.Background(), name, options) @@ -62,6 +83,7 @@ func (cli *DockerCli) CmdSearch(args ...string) error { w := tabwriter.NewWriter(cli.out, 10, 1, 3, ' ', 0) fmt.Fprintf(w, "NAME\tDESCRIPTION\tSTARS\tOFFICIAL\tAUTOMATED\n") for _, res := range results { + // --automated and -s, --stars are deprecated since Docker 1.12 if (*automated && !res.IsAutomated) || (int(*stars) > res.StarCount) { continue } diff --git a/api/server/router/image/backend.go b/api/server/router/image/backend.go index 7e5a6b6436..cc854a2768 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, term string, authConfig *types.AuthConfig, metaHeaders map[string][]string) (*registry.SearchResults, error) + SearchRegistryForImages(ctx context.Context, filtersArgs string, term string, 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 b9168bc89d..3b8180f214 100644 --- a/api/server/router/image/image_routes.go +++ b/api/server/router/image/image_routes.go @@ -301,7 +301,7 @@ func (s *imageRouter) getImagesSearch(ctx context.Context, w http.ResponseWriter headers[k] = v } } - query, err := s.backend.SearchRegistryForImages(ctx, r.Form.Get("term"), config, headers) + query, err := s.backend.SearchRegistryForImages(ctx, r.Form.Get("filters"), r.Form.Get("term"), config, headers) if err != nil { return err } diff --git a/contrib/completion/bash/docker b/contrib/completion/bash/docker index f26ff80244..0bea86cb8d 100644 --- a/contrib/completion/bash/docker +++ b/contrib/completion/bash/docker @@ -1907,15 +1907,29 @@ _docker_save() { } _docker_search() { + local key=$(__docker_map_key_of_current_option '--filter|-f') + case "$key" in + is-automated) + COMPREPLY=( $( compgen -W "false true" -- "${cur##*=}" ) ) + return + ;; + is-official) + COMPREPLY=( $( compgen -W "false true" -- "${cur##*=}" ) ) + return + ;; + esac + case "$prev" in - --stars|-s) + --filter|-f) + COMPREPLY=( $( compgen -S = -W "is-automated is-official stars" -- "$cur" ) ) + __docker_nospace return ;; esac case "$cur" in -*) - COMPREPLY=( $( compgen -W "--automated --help --no-trunc --stars -s" -- "$cur" ) ) + COMPREPLY=( $( compgen -W "--filter --help --no-trunc" -- "$cur" ) ) ;; esac } diff --git a/contrib/completion/zsh/_docker b/contrib/completion/zsh/_docker index 5862768ff3..3e0f26d504 100644 --- a/contrib/completion/zsh/_docker +++ b/contrib/completion/zsh/_docker @@ -311,6 +311,30 @@ __docker_complete_ps_filters() { return ret } +__docker_complete_search_filters() { + [[ $PREFIX = -* ]] && return 1 + integer ret=1 + declare -a boolean_opts opts + + boolean_opts=('true' 'false') + opts=('is-automated' 'is-official' 'stars') + + if compset -P '*='; then + case "${${words[-1]%=*}#*=}" in + (is-automated|is-official) + _describe -t boolean-filter-opts "filter options" boolean_opts && ret=0 + ;; + *) + _message 'value' && ret=0 + ;; + esac + else + _describe -t filter-opts "filter options" opts -qS "=" && ret=0 + fi + + return ret +} + __docker_network_complete_ls_filters() { [[ $PREFIX = -* ]] && return 1 integer ret=1 @@ -1126,10 +1150,15 @@ __docker_subcommand() { (search) _arguments $(__docker_arguments) \ $opts_help \ - "($help)--automated[Only show automated builds]" \ + "($help)*"{-f=,--filter=}"[Filter values]:filter:->filter-options" \ "($help)--no-trunc[Do not truncate output]" \ - "($help -s --stars)"{-s=,--stars=}"[Only display with at least X stars]:stars:(0 10 100 1000)" \ "($help -):term: " && ret=0 + + case $state in + (filter-options) + __docker_complete_search_filters && ret=0 + ;; + esac ;; (start) _arguments $(__docker_arguments) \ diff --git a/daemon/daemon.go b/daemon/daemon.go index 524aeb8b60..0068f03cd9 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -15,6 +15,7 @@ import ( "path/filepath" "regexp" "runtime" + "strconv" "strings" "sync" "syscall" @@ -64,6 +65,7 @@ import ( volumedrivers "github.com/docker/docker/volume/drivers" "github.com/docker/docker/volume/local" "github.com/docker/docker/volume/store" + "github.com/docker/engine-api/types/filters" "github.com/docker/go-connections/nat" "github.com/docker/libnetwork" nwconfig "github.com/docker/libnetwork/config" @@ -1427,12 +1429,85 @@ func (daemon *Daemon) AuthenticateToRegistry(ctx context.Context, authConfig *ty return daemon.RegistryService.Auth(authConfig, dockerversion.DockerUserAgent(ctx)) } +var acceptedSearchFilterTags = map[string]bool{ + "is-automated": true, + "is-official": true, + "stars": true, +} + // SearchRegistryForImages queries the registry for images matching // term. authConfig is used to login. -func (daemon *Daemon) SearchRegistryForImages(ctx context.Context, term string, +func (daemon *Daemon) SearchRegistryForImages(ctx context.Context, filtersArgs string, term string, authConfig *types.AuthConfig, headers map[string][]string) (*registrytypes.SearchResults, error) { - return daemon.RegistryService.Search(term, authConfig, dockerversion.DockerUserAgent(ctx), headers) + + searchFilters, err := filters.FromParam(filtersArgs) + if err != nil { + return nil, err + } + if err := searchFilters.Validate(acceptedSearchFilterTags); err != nil { + return nil, err + } + + unfilteredResult, err := daemon.RegistryService.Search(term, authConfig, dockerversion.DockerUserAgent(ctx), headers) + if err != nil { + return nil, err + } + + var isAutomated, isOfficial bool + var hasStarFilter = 0 + if searchFilters.Include("is-automated") { + if searchFilters.ExactMatch("is-automated", "true") { + isAutomated = true + } else if !searchFilters.ExactMatch("is-automated", "false") { + return nil, fmt.Errorf("Invalid filter 'is-automated=%s'", searchFilters.Get("is-automated")) + } + } + if searchFilters.Include("is-official") { + if searchFilters.ExactMatch("is-official", "true") { + isOfficial = true + } else if !searchFilters.ExactMatch("is-official", "false") { + return nil, fmt.Errorf("Invalid filter 'is-official=%s'", searchFilters.Get("is-official")) + } + } + if searchFilters.Include("stars") { + hasStars := searchFilters.Get("stars") + for _, hasStar := range hasStars { + iHasStar, err := strconv.Atoi(hasStar) + if err != nil { + return nil, fmt.Errorf("Invalid filter 'stars=%s'", hasStar) + } + if iHasStar > hasStarFilter { + hasStarFilter = iHasStar + } + } + } + + filteredResults := []registrytypes.SearchResult{} + for _, result := range unfilteredResult.Results { + if searchFilters.Include("is-automated") { + if isAutomated != result.IsAutomated { + continue + } + } + if searchFilters.Include("is-official") { + if isOfficial != result.IsOfficial { + continue + } + } + if searchFilters.Include("stars") { + if result.StarCount < hasStarFilter { + continue + } + } + filteredResults = append(filteredResults, result) + } + + return ®istrytypes.SearchResults{ + Query: unfilteredResult.Query, + NumResults: len(filteredResults), + Results: filteredResults, + }, nil } // IsShuttingDown tells whether the daemon is shutting down or not diff --git a/docs/deprecated.md b/docs/deprecated.md index df87118478..d80f24e729 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -58,6 +58,15 @@ defining it at container creation (`POST /containers/create`). The `docker ps --before` and `docker ps --since` options are deprecated. Use `docker ps --filter=before=...` and `docker ps --filter=since=...` instead. +### Docker search 'automated' and 'stars' options + +**Deprecated in Release: [v1.12.0](https://github.com/docker/docker/releases/tag/v1.12.0)** + +**Removed In Release: v1.14** + +The `docker search --automated` and `docker search --stars` options are deprecated. +Use `docker search --filter=is-automated=...` and `docker search --filter=stars=...` instead. + ### Command line short variant options **Deprecated In Release: v1.9** diff --git a/docs/reference/api/docker_remote_api.md b/docs/reference/api/docker_remote_api.md index 3e6f197451..386d429080 100644 --- a/docs/reference/api/docker_remote_api.md +++ b/docs/reference/api/docker_remote_api.md @@ -118,6 +118,7 @@ This section lists each version from latest to oldest. Each listing includes a * `POST /containers/create` now takes `MaximumIOps` and `MaximumIOBps` fields. Windows daemon only. * `POST /containers/create` now returns a HTTP 400 "bad parameter" message if no command is specified (instead of a HTTP 500 "server error") +* `GET /images/search` now takes a `filters` query parameter. ### 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 aa47ef7f2a..088fb09738 100644 --- a/docs/reference/api/docker_remote_api_v1.24.md +++ b/docs/reference/api/docker_remote_api_v1.24.md @@ -2133,6 +2133,10 @@ Search for an image on [Docker Hub](https://hub.docker.com). Query Parameters: - **term** – term to search +- **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)` + - `is-official=(true|false)` Status Codes: diff --git a/docs/reference/commandline/search.md b/docs/reference/commandline/search.md index 893895e214..8bca98f0c9 100644 --- a/docs/reference/commandline/search.md +++ b/docs/reference/commandline/search.md @@ -14,10 +14,12 @@ parent = "smn_cli" Search the Docker Hub for images - --automated Only show automated builds + --filter=[] Filter output based on these conditions: + - is-automated=(true|false) + - is-official=(true|false) + - stars= - image has at least 'number' stars --help Print usage --no-trunc Don't truncate output - -s, --stars=0 Only displays with at least x stars Search [Docker Hub](https://hub.docker.com) for images @@ -61,29 +63,6 @@ This example displays images with a name containing 'busybox': scottabernethy/busybox 0 [OK] marclop/busybox-solr -### Search images by name and number of stars (-s, --stars) - -This example displays images with a name containing 'busybox' and at -least 3 stars: - - $ docker search --stars=3 busybox - NAME DESCRIPTION STARS OFFICIAL AUTOMATED - busybox Busybox base image. 325 [OK] - progrium/busybox 50 [OK] - radial/busyboxplus Full-chain, Internet enabled, busybox made... 8 [OK] - - -### Search automated images (--automated) - -This example displays images with a name containing 'busybox', at -least 3 stars and are automated builds: - - $ docker search --stars=3 --automated busybox - NAME DESCRIPTION STARS OFFICIAL AUTOMATED - progrium/busybox 50 [OK] - radial/busyboxplus Full-chain, Internet enabled, busybox made... 8 [OK] - - ### Display non-truncated description (--no-trunc) This example displays images with a name containing 'busybox', @@ -95,3 +74,48 @@ 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] +## Filtering + +The filtering flag (`-f` or `--filter`) format is a `key=value` pair. If there is more +than one filter, then pass multiple flags (e.g. `--filter "foo=bar" --filter "bif=baz"`) + +The currently supported filters are: + +* stars (int - number of stars the image has) +* is-automated (true|false) - is the image automated or not +* is-official (true|false) - is the image official or not + + +### stars + +This example displays images with a name containing 'busybox' and at +least 3 stars: + + $ docker search --filter stars=3 busybox + NAME DESCRIPTION STARS OFFICIAL AUTOMATED + busybox Busybox base image. 325 [OK] + progrium/busybox 50 [OK] + radial/busyboxplus Full-chain, Internet enabled, busybox made... 8 [OK] + + +### is-automated + +This example displays images with a name containing 'busybox' +and are automated builds: + + $ docker search --filter is-automated busybox + NAME DESCRIPTION STARS OFFICIAL AUTOMATED + progrium/busybox 50 [OK] + radial/busyboxplus Full-chain, Internet enabled, busybox made... 8 [OK] + +### is-official + +This example displays images with a name containing 'busybox', at least +3 stars and are official builds: + + $ docker search --filter "is-automated=true" --filter "stars=3" busybox + NAME DESCRIPTION STARS OFFICIAL AUTOMATED + progrium/busybox 50 [OK] + radial/busyboxplus Full-chain, Internet enabled, busybox made... 8 [OK] + + diff --git a/integration-cli/docker_cli_search_test.go b/integration-cli/docker_cli_search_test.go index dfab81044a..a93d657265 100644 --- a/integration-cli/docker_cli_search_test.go +++ b/integration-cli/docker_cli_search_test.go @@ -16,34 +16,78 @@ func (s *DockerSuite) TestSearchOnCentralRegistry(c *check.C) { } func (s *DockerSuite) TestSearchStarsOptionWithWrongParameter(c *check.C) { - out, _, err := dockerCmdWithError("search", "--stars=a", "busybox") + out, _, err := dockerCmdWithError("search", "--filter", "stars=a", "busybox") + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(out, checker.Contains, "Invalid filter", check.Commentf("couldn't find the invalid filter warning")) + + out, _, err = dockerCmdWithError("search", "-f", "stars=a", "busybox") + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(out, checker.Contains, "Invalid filter", check.Commentf("couldn't find the invalid filter warning")) + + out, _, err = dockerCmdWithError("search", "-f", "is-automated=a", "busybox") + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(out, checker.Contains, "Invalid filter", check.Commentf("couldn't find the invalid filter warning")) + + out, _, err = dockerCmdWithError("search", "-f", "is-official=a", "busybox") + c.Assert(err, check.NotNil, check.Commentf(out)) + c.Assert(out, checker.Contains, "Invalid filter", check.Commentf("couldn't find the invalid filter warning")) + + // -s --stars deprecated since Docker 1.13 + out, _, err = dockerCmdWithError("search", "--stars=a", "busybox") c.Assert(err, check.NotNil, check.Commentf(out)) c.Assert(out, checker.Contains, "invalid value", check.Commentf("couldn't find the invalid value warning")) + // -s --stars deprecated since Docker 1.13 out, _, err = dockerCmdWithError("search", "-s=-1", "busybox") c.Assert(err, check.NotNil, check.Commentf(out)) c.Assert(out, checker.Contains, "invalid value", check.Commentf("couldn't find the invalid value warning")) } func (s *DockerSuite) TestSearchCmdOptions(c *check.C) { - testRequires(c, Network) + testRequires(c, Network, DaemonIsLinux) out, _ := dockerCmd(c, "search", "--help") c.Assert(out, checker.Contains, "Usage:\tdocker search [OPTIONS] TERM") outSearchCmd, _ := dockerCmd(c, "search", "busybox") outSearchCmdNotrunc, _ := dockerCmd(c, "search", "--no-trunc=true", "busybox") + c.Assert(len(outSearchCmd) > len(outSearchCmdNotrunc), check.Equals, false, check.Commentf("The no-trunc option can't take effect.")) - outSearchCmdautomated, _ := dockerCmd(c, "search", "--automated=true", "busybox") //The busybox is a busybox base image, not an AUTOMATED image. + outSearchCmdautomated, _ := dockerCmd(c, "search", "--filter", "is-automated=true", "busybox") //The busybox is a busybox base image, not an AUTOMATED image. outSearchCmdautomatedSlice := strings.Split(outSearchCmdautomated, "\n") for i := range outSearchCmdautomatedSlice { - c.Assert(strings.HasPrefix(outSearchCmdautomatedSlice[i], "busybox "), check.Equals, false, check.Commentf("The busybox is not an AUTOMATED image: %s", out)) + c.Assert(strings.HasPrefix(outSearchCmdautomatedSlice[i], "busybox "), check.Equals, false, check.Commentf("The busybox is not an AUTOMATED image: %s", outSearchCmdautomated)) } - outSearchCmdStars, _ := dockerCmd(c, "search", "-s=2", "busybox") + outSearchCmdNotOfficial, _ := dockerCmd(c, "search", "--filter", "is-official=false", "busybox") //The busybox is a busybox base image, official image. + outSearchCmdNotOfficialSlice := strings.Split(outSearchCmdNotOfficial, "\n") + for i := range outSearchCmdNotOfficialSlice { + c.Assert(strings.HasPrefix(outSearchCmdNotOfficialSlice[i], "busybox "), check.Equals, false, check.Commentf("The busybox is not an OFFICIAL image: %s", outSearchCmdNotOfficial)) + } + + outSearchCmdOfficial, _ := dockerCmd(c, "search", "--filter", "is-official=true", "busybox") //The busybox is a busybox base image, official image. + outSearchCmdOfficialSlice := strings.Split(outSearchCmdOfficial, "\n") + c.Assert(outSearchCmdOfficialSlice, checker.HasLen, 3) // 1 header, 1 line, 1 carriage return + c.Assert(strings.HasPrefix(outSearchCmdOfficialSlice[1], "busybox "), check.Equals, true, check.Commentf("The busybox is an OFFICIAL image: %s", outSearchCmdNotOfficial)) + + outSearchCmdStars, _ := dockerCmd(c, "search", "--filter", "stars=2", "busybox") c.Assert(strings.Count(outSearchCmdStars, "[OK]") > strings.Count(outSearchCmd, "[OK]"), check.Equals, false, check.Commentf("The quantity of images with stars should be less than that of all images: %s", outSearchCmdStars)) + dockerCmd(c, "search", "--filter", "is-automated=true", "--filter", "stars=2", "--no-trunc=true", "busybox") + + // --automated deprecated since Docker 1.13 + outSearchCmdautomated1, _ := dockerCmd(c, "search", "--automated=true", "busybox") //The busybox is a busybox base image, not an AUTOMATED image. + outSearchCmdautomatedSlice1 := strings.Split(outSearchCmdautomated1, "\n") + for i := range outSearchCmdautomatedSlice1 { + c.Assert(strings.HasPrefix(outSearchCmdautomatedSlice1[i], "busybox "), check.Equals, false, check.Commentf("The busybox is not an AUTOMATED image: %s", outSearchCmdautomated)) + } + + // -s --stars deprecated since Docker 1.13 + outSearchCmdStars1, _ := dockerCmd(c, "search", "--stars=2", "busybox") + c.Assert(strings.Count(outSearchCmdStars1, "[OK]") > strings.Count(outSearchCmd, "[OK]"), check.Equals, false, check.Commentf("The quantity of images with stars should be less than that of all images: %s", outSearchCmdStars1)) + + // -s --stars deprecated since Docker 1.13 dockerCmd(c, "search", "--stars=2", "--automated=true", "--no-trunc=true", "busybox") } diff --git a/man/docker-search.1.md b/man/docker-search.1.md index a95c023773..c1728f548c 100644 --- a/man/docker-search.1.md +++ b/man/docker-search.1.md @@ -6,10 +6,9 @@ docker-search - Search the Docker Hub for images # SYNOPSIS **docker search** -[**--automated**] +[**-f**|**--filter**[=*[]*]] [**--help**] [**--no-trunc**] -[**-s**|**--stars**[=*0*]] TERM # DESCRIPTION @@ -21,8 +20,12 @@ of stars awarded, whether the image is official, and whether it is automated. *Note* - Search queries will only return up to 25 results # OPTIONS -**--automated**=*true*|*false* - Only show automated builds. The default is *false*. + +**-f**, **--filter**=[] + Filter output based on these conditions: + - stars= + - is-automated=(true|false) + - is-official=(true|false) **--help** Print usage statement @@ -30,9 +33,6 @@ of stars awarded, whether the image is official, and whether it is automated. **--no-trunc**=*true*|*false* Don't truncate output. The default is *false*. -**-s**, **--stars**=*X* - Only displays with at least X stars. The default is zero. - # EXAMPLES ## Search Docker Hub for ranked images @@ -40,7 +40,7 @@ of stars awarded, whether the image is official, and whether it is automated. Search a registry for the term 'fedora' and only display those images ranked 3 or higher: - $ docker search -s 3 fedora + $ docker search --filter=stars=3 fedora NAME DESCRIPTION STARS OFFICIAL AUTOMATED mattdm/fedora A basic Fedora image corresponding roughly... 50 fedora (Semi) Official Fedora base image. 38 @@ -52,7 +52,7 @@ ranked 3 or higher: Search Docker Hub for the term 'fedora' and only display automated images ranked 1 or higher: - $ docker search --automated -s 1 fedora + $ docker search --filter=is-automated=true --filter=stars=1 fedora NAME DESCRIPTION STARS OFFICIAL AUTOMATED goldmann/wildfly A WildFly application server running on a ... 3 [OK] tutum/fedora-20 Fedora 20 image with SSH access. For the r... 1 [OK] @@ -62,4 +62,5 @@ April 2014, Originally compiled by William Henry (whenry at redhat dot com) based on docker.com source material and internal work. June 2014, updated by Sven Dowideit April 2015, updated by Mary Anthony for v2 +April 2016, updated by Vincent Demeester