diff --git a/api/client/commands.go b/api/client/commands.go index 1031f92bb2..2296a4a4c3 100644 --- a/api/client/commands.go +++ b/api/client/commands.go @@ -40,7 +40,6 @@ func (cli *DockerCli) Command(name string) func(...string) error { "rmi": cli.CmdRmi, "run": cli.CmdRun, "save": cli.CmdSave, - "search": cli.CmdSearch, "start": cli.CmdStart, "stats": cli.CmdStats, "stop": cli.CmdStop, diff --git a/api/client/create.go b/api/client/create.go index 18903152e0..c81d6fb3c1 100644 --- a/api/client/create.go +++ b/api/client/create.go @@ -31,8 +31,8 @@ func (cli *DockerCli) pullImage(ctx context.Context, image string, out io.Writer return err } - authConfig := cli.resolveAuthConfig(ctx, repoInfo.Index) - encodedAuth, err := encodeAuthToBase64(authConfig) + authConfig := cli.ResolveAuthConfig(ctx, repoInfo.Index) + encodedAuth, err := EncodeAuthToBase64(authConfig) if err != nil { return err } diff --git a/api/client/image/search.go b/api/client/image/search.go new file mode 100644 index 0000000000..dede522cca --- /dev/null +++ b/api/client/image/search.go @@ -0,0 +1,135 @@ +package image + +import ( + "fmt" + "sort" + "strings" + "text/tabwriter" + + "golang.org/x/net/context" + + "github.com/docker/docker/api/client" + "github.com/docker/docker/cli" + "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" + "github.com/spf13/cobra" +) + +type searchOptions struct { + term string + noTrunc bool + limit int + filter []string + + // Deprecated + stars uint + automated bool +} + +// NewSearchCommand create a new `docker search` command +func NewSearchCommand(dockerCli *client.DockerCli) *cobra.Command { + var opts searchOptions + + cmd := &cobra.Command{ + Use: "search [OPTIONS] TERM", + Short: "Search the Docker Hub for images", + Args: cli.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + opts.term = args[0] + return runSearch(dockerCli, opts) + }, + } + + flags := cmd.Flags() + + flags.BoolVar(&opts.noTrunc, "no-trunc", false, "Don't truncate output") + flags.StringSliceVarP(&opts.filter, "filter", "f", []string{}, "Filter output based on conditions provided") + flags.IntVar(&opts.limit, "limit", registry.DefaultSearchLimit, "Max number of search results") + + flags.BoolVar(&opts.automated, "automated", false, "Only show automated builds") + flags.UintVarP(&opts.stars, "stars", "s", 0, "Only displays with at least x stars") + + flags.MarkDeprecated("automated", "Use --filter=automated=true instead") + flags.MarkDeprecated("stars", "Use --filter=stars=3 instead") + + return cmd +} + +func runSearch(dockerCli *client.DockerCli, opts searchOptions) error { + indexInfo, err := registry.ParseSearchIndexInfo(opts.term) + if err != nil { + return err + } + + ctx := context.Background() + + authConfig := dockerCli.ResolveAuthConfig(ctx, indexInfo) + requestPrivilege := dockerCli.RegistryAuthenticationPrivilegedFunc(indexInfo, "search") + + encodedAuth, err := client.EncodeAuthToBase64(authConfig) + if err != nil { + return err + } + + searchFilters := filters.NewArgs() + for _, f := range opts.filter { + var err error + searchFilters, err = filters.ParseFlag(f, searchFilters) + if err != nil { + return err + } + } + + options := types.ImageSearchOptions{ + RegistryAuth: encodedAuth, + PrivilegeFunc: requestPrivilege, + Filters: searchFilters, + Limit: opts.limit, + } + + clnt := dockerCli.Client() + + unorderedResults, err := clnt.ImageSearch(ctx, opts.term, options) + if err != nil { + return err + } + + results := searchResultsByStars(unorderedResults) + sort.Sort(results) + + w := tabwriter.NewWriter(dockerCli.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 (opts.automated && !res.IsAutomated) || (int(opts.stars) > res.StarCount) { + continue + } + desc := strings.Replace(res.Description, "\n", " ", -1) + desc = strings.Replace(desc, "\r", " ", -1) + if !opts.noTrunc && len(desc) > 45 { + desc = stringutils.Truncate(desc, 42) + "..." + } + fmt.Fprintf(w, "%s\t%s\t%d\t", res.Name, desc, res.StarCount) + if res.IsOfficial { + fmt.Fprint(w, "[OK]") + + } + fmt.Fprint(w, "\t") + if res.IsAutomated { + fmt.Fprint(w, "[OK]") + } + fmt.Fprint(w, "\n") + } + w.Flush() + return nil +} + +// SearchResultsByStars sorts search results in descending order by number of stars. +type searchResultsByStars []registrytypes.SearchResult + +func (r searchResultsByStars) Len() int { return len(r) } +func (r searchResultsByStars) Swap(i, j int) { r[i], r[j] = r[j], r[i] } +func (r searchResultsByStars) Less(i, j int) bool { return r[j].StarCount < r[i].StarCount } diff --git a/api/client/pull.go b/api/client/pull.go index d618dce905..a3aca027be 100644 --- a/api/client/pull.go +++ b/api/client/pull.go @@ -57,8 +57,8 @@ func (cli *DockerCli) CmdPull(args ...string) error { ctx := context.Background() - authConfig := cli.resolveAuthConfig(ctx, repoInfo.Index) - requestPrivilege := cli.registryAuthenticationPrivilegedFunc(repoInfo.Index, "pull") + authConfig := cli.ResolveAuthConfig(ctx, repoInfo.Index) + requestPrivilege := cli.RegistryAuthenticationPrivilegedFunc(repoInfo.Index, "pull") if isTrusted() && !registryRef.HasDigest() { // Check if tag is digest @@ -70,7 +70,7 @@ func (cli *DockerCli) CmdPull(args ...string) error { func (cli *DockerCli) imagePullPrivileged(ctx context.Context, authConfig types.AuthConfig, ref string, requestPrivilege types.RequestPrivilegeFunc, all bool) error { - encodedAuth, err := encodeAuthToBase64(authConfig) + encodedAuth, err := EncodeAuthToBase64(authConfig) if err != nil { return err } diff --git a/api/client/push.go b/api/client/push.go index 903f85f766..8dc695b542 100644 --- a/api/client/push.go +++ b/api/client/push.go @@ -37,8 +37,8 @@ func (cli *DockerCli) CmdPush(args ...string) error { ctx := context.Background() // Resolve the Auth config relevant for this server - authConfig := cli.resolveAuthConfig(ctx, repoInfo.Index) - requestPrivilege := cli.registryAuthenticationPrivilegedFunc(repoInfo.Index, "push") + authConfig := cli.ResolveAuthConfig(ctx, repoInfo.Index) + requestPrivilege := cli.RegistryAuthenticationPrivilegedFunc(repoInfo.Index, "push") if isTrusted() { return cli.trustedPush(ctx, repoInfo, ref, authConfig, requestPrivilege) @@ -55,7 +55,7 @@ func (cli *DockerCli) CmdPush(args ...string) error { } func (cli *DockerCli) imagePushPrivileged(ctx context.Context, authConfig types.AuthConfig, ref string, requestPrivilege types.RequestPrivilegeFunc) (io.ReadCloser, error) { - encodedAuth, err := encodeAuthToBase64(authConfig) + encodedAuth, err := EncodeAuthToBase64(authConfig) if err != nil { return nil, err } diff --git a/api/client/search.go b/api/client/search.go deleted file mode 100644 index d2d1446239..0000000000 --- a/api/client/search.go +++ /dev/null @@ -1,119 +0,0 @@ -package client - -import ( - "fmt" - "net/url" - "sort" - "strings" - "text/tabwriter" - - "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" -) - -// CmdSearch searches the Docker Hub for images. -// -// 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") - 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" - 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) - - indexInfo, err := registry.ParseSearchIndexInfo(name) - if err != nil { - return err - } - - ctx := context.Background() - - authConfig := cli.resolveAuthConfig(ctx, indexInfo) - requestPrivilege := cli.registryAuthenticationPrivilegedFunc(indexInfo, "search") - - encodedAuth, err := encodeAuthToBase64(authConfig) - if err != nil { - return err - } - - options := types.ImageSearchOptions{ - RegistryAuth: encodedAuth, - PrivilegeFunc: requestPrivilege, - Filters: filterArgs, - Limit: *flLimit, - } - - unorderedResults, err := cli.client.ImageSearch(ctx, name, options) - if err != nil { - return err - } - - results := searchResultsByStars(unorderedResults) - sort.Sort(results) - - 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 - } - desc := strings.Replace(res.Description, "\n", " ", -1) - desc = strings.Replace(desc, "\r", " ", -1) - if !*noTrunc && len(desc) > 45 { - desc = stringutils.Truncate(desc, 42) + "..." - } - fmt.Fprintf(w, "%s\t%s\t%d\t", res.Name, desc, res.StarCount) - if res.IsOfficial { - fmt.Fprint(w, "[OK]") - - } - fmt.Fprint(w, "\t") - if res.IsAutomated { - fmt.Fprint(w, "[OK]") - } - fmt.Fprint(w, "\n") - } - w.Flush() - return nil -} - -// SearchResultsByStars sorts search results in descending order by number of stars. -type searchResultsByStars []registrytypes.SearchResult - -func (r searchResultsByStars) Len() int { return len(r) } -func (r searchResultsByStars) Swap(i, j int) { r[i], r[j] = r[j], r[i] } -func (r searchResultsByStars) Less(i, j int) bool { return r[j].StarCount < r[i].StarCount } diff --git a/api/client/trust.go b/api/client/trust.go index aa2c4a8d51..87b7ce5658 100644 --- a/api/client/trust.go +++ b/api/client/trust.go @@ -236,7 +236,7 @@ func (cli *DockerCli) trustedReference(ctx context.Context, ref reference.NamedT } // Resolve the Auth config relevant for this server - authConfig := cli.resolveAuthConfig(ctx, repoInfo.Index) + authConfig := cli.ResolveAuthConfig(ctx, repoInfo.Index) notaryRepo, err := cli.getNotaryRepository(repoInfo, authConfig, "pull") if err != nil { diff --git a/api/client/utils.go b/api/client/utils.go index fe057856ee..455b5d1afd 100644 --- a/api/client/utils.go +++ b/api/client/utils.go @@ -37,8 +37,8 @@ func (cli *DockerCli) electAuthServer(ctx context.Context) string { return serverAddress } -// encodeAuthToBase64 serializes the auth configuration as JSON base64 payload -func encodeAuthToBase64(authConfig types.AuthConfig) (string, error) { +// EncodeAuthToBase64 serializes the auth configuration as JSON base64 payload +func EncodeAuthToBase64(authConfig types.AuthConfig) (string, error) { buf, err := json.Marshal(authConfig) if err != nil { return "", err @@ -46,7 +46,9 @@ func encodeAuthToBase64(authConfig types.AuthConfig) (string, error) { return base64.URLEncoding.EncodeToString(buf), nil } -func (cli *DockerCli) registryAuthenticationPrivilegedFunc(index *registrytypes.IndexInfo, cmdName string) types.RequestPrivilegeFunc { +// RegistryAuthenticationPrivilegedFunc return a RequestPrivilegeFunc from the specified registry index info +// for the given command. +func (cli *DockerCli) RegistryAuthenticationPrivilegedFunc(index *registrytypes.IndexInfo, cmdName string) types.RequestPrivilegeFunc { return func() (string, error) { fmt.Fprintf(cli.out, "\nPlease login prior to %s:\n", cmdName) indexServer := registry.GetAuthConfigKey(index) @@ -54,7 +56,7 @@ func (cli *DockerCli) registryAuthenticationPrivilegedFunc(index *registrytypes. if err != nil { return "", err } - return encodeAuthToBase64(authConfig) + return EncodeAuthToBase64(authConfig) } } @@ -182,10 +184,10 @@ func copyToFile(outfile string, r io.Reader) error { return nil } -// resolveAuthConfig is like registry.ResolveAuthConfig, but if using the +// ResolveAuthConfig is like registry.ResolveAuthConfig, but if using the // default index, it uses the default index name for the daemon's platform, // not the client's platform. -func (cli *DockerCli) resolveAuthConfig(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig { +func (cli *DockerCli) ResolveAuthConfig(ctx context.Context, index *registrytypes.IndexInfo) types.AuthConfig { configKey := index.Name if index.Official { configKey = cli.electAuthServer(ctx) diff --git a/cli/cobraadaptor/adaptor.go b/cli/cobraadaptor/adaptor.go index db16166f09..633844a12a 100644 --- a/cli/cobraadaptor/adaptor.go +++ b/cli/cobraadaptor/adaptor.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/docker/docker/api/client" + "github.com/docker/docker/api/client/image" "github.com/docker/docker/api/client/volume" "github.com/docker/docker/cli" cliflags "github.com/docker/docker/cli/flags" @@ -34,6 +35,7 @@ func NewCobraAdaptor(clientFlags *cliflags.ClientFlags) CobraAdaptor { rootCmd.SetOutput(stdout) rootCmd.AddCommand( volume.NewVolumeCommand(dockerCli), + image.NewSearchCommand(dockerCli), ) rootCmd.PersistentFlags().BoolP("help", "h", false, "Print usage") diff --git a/cli/required.go b/cli/required.go index 94710374e6..b3ebb9ba9f 100644 --- a/cli/required.go +++ b/cli/required.go @@ -40,3 +40,19 @@ func RequiresMinArgs(min int) cobra.PositionalArgs { ) } } + +// ExactArgs returns an error if there is not the exact number of args +func ExactArgs(number int) cobra.PositionalArgs { + return func(cmd *cobra.Command, args []string) error { + if len(args) == number { + return nil + } + return fmt.Errorf( + "\"%s\" requires exactly %d argument(s).\n\nUsage: %s\n\n%s", + cmd.CommandPath(), + number, + cmd.UseLine(), + cmd.Short, + ) + } +} diff --git a/cli/usage.go b/cli/usage.go index 9ddc17326a..98d7fdf441 100644 --- a/cli/usage.go +++ b/cli/usage.go @@ -39,7 +39,6 @@ var DockerCommandUsage = []Command{ {"rmi", "Remove one or more images"}, {"run", "Run a command in a new container"}, {"save", "Save one or more images to a tar archive"}, - {"search", "Search the Docker Hub for images"}, {"start", "Start one or more stopped containers"}, {"stats", "Display a live stream of container(s) resource usage statistics"}, {"stop", "Stop a running container"}, diff --git a/integration-cli/docker_cli_search_test.go b/integration-cli/docker_cli_search_test.go index ba8d0021a7..5a32f2ab93 100644 --- a/integration-cli/docker_cli_search_test.go +++ b/integration-cli/docker_cli_search_test.go @@ -36,12 +36,12 @@ func (s *DockerSuite) TestSearchStarsOptionWithWrongParameter(c *check.C) { // -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")) + c.Assert(out, checker.Contains, "invalid syntax", 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")) + c.Assert(out, checker.Contains, "invalid syntax", check.Commentf("couldn't find the invalid value warning")) } func (s *DockerSuite) TestSearchCmdOptions(c *check.C) {