aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorumohnani8 <umohnani@redhat.com>2018-01-10 09:35:23 -0500
committerAtomic Bot <atomic-devel@projectatomic.io>2018-02-06 18:09:31 +0000
commit0d7e6fa22fa15254e1599b9f41ab9be2650cff71 (patch)
treebf7997d2e3259788a79b4ebcfab3dab1e8f655a3
parent1a48a7a7c0dafe98ed4c8461adabc6b8ece1a8c3 (diff)
downloadpodman-0d7e6fa22fa15254e1599b9f41ab9be2650cff71.tar.gz
podman-0d7e6fa22fa15254e1599b9f41ab9be2650cff71.tar.bz2
podman-0d7e6fa22fa15254e1599b9f41ab9be2650cff71.zip
Add podman search command
podman search queries a registry for a matching image and prints the output. I added a new flag called "registry" giving the user the option to search a specific registry if they don't want to search all their default registries. Signed-off-by: umohnani8 <umohnani@redhat.com> Closes: #241 Approved by: rhatdan
-rw-r--r--cmd/podman/main.go1
-rw-r--r--cmd/podman/search.go290
-rw-r--r--completions/bash/podman14
-rw-r--r--docs/podman-search.1.md115
-rw-r--r--test/podman_search.bats43
-rw-r--r--transfer.md2
6 files changed, 464 insertions, 1 deletions
diff --git a/cmd/podman/main.go b/cmd/podman/main.go
index bda8ff517..f18615760 100644
--- a/cmd/podman/main.go
+++ b/cmd/podman/main.go
@@ -64,6 +64,7 @@ func main() {
rmiCommand,
runCommand,
saveCommand,
+ searchCommand,
startCommand,
statsCommand,
stopCommand,
diff --git a/cmd/podman/search.go b/cmd/podman/search.go
new file mode 100644
index 000000000..01eaa6729
--- /dev/null
+++ b/cmd/podman/search.go
@@ -0,0 +1,290 @@
+package main
+
+import (
+ "context"
+ "reflect"
+ "strconv"
+ "strings"
+
+ "github.com/containers/image/docker"
+ "github.com/pkg/errors"
+ "github.com/projectatomic/libpod/cmd/podman/formats"
+ "github.com/projectatomic/libpod/libpod"
+ "github.com/projectatomic/libpod/libpod/common"
+ "github.com/sirupsen/logrus"
+ "github.com/urfave/cli"
+)
+
+const (
+ descriptionTruncLength = 44
+ maxQueries = 25
+)
+
+var (
+ searchFlags = []cli.Flag{
+ cli.StringSliceFlag{
+ Name: "filter, f",
+ Usage: "filter output based on conditions provided (default [])",
+ },
+ cli.StringFlag{
+ Name: "format",
+ Usage: "change the output format to a Go template",
+ },
+ cli.IntFlag{
+ Name: "limit",
+ Usage: "limit the number of results",
+ },
+ cli.BoolFlag{
+ Name: "no-trunc",
+ Usage: "do not truncate the output",
+ },
+ cli.StringSliceFlag{
+ Name: "registry",
+ Usage: "specific registry to search",
+ },
+ }
+ searchDescription = `
+ Search registries for a given image. Can search all the default registries or a specific registry.
+ Can limit the number of results, and filter the output based on certain conditions.`
+ searchCommand = cli.Command{
+ Name: "search",
+ Usage: "search registry for image",
+ Description: searchDescription,
+ Flags: searchFlags,
+ Action: searchCmd,
+ ArgsUsage: "TERM",
+ }
+)
+
+type searchParams struct {
+ Index string
+ Name string
+ Description string
+ Stars int
+ Official string
+ Automated string
+}
+
+type searchOpts struct {
+ filter []string
+ limit int
+ noTrunc bool
+ format string
+}
+
+type searchFilterParams struct {
+ stars int
+ isAutomated *bool
+ isOfficial *bool
+}
+
+func searchCmd(c *cli.Context) error {
+ args := c.Args()
+ if len(args) > 1 {
+ return errors.Errorf("too many arguments. Requires exactly 1")
+ }
+ if len(args) == 0 {
+ return errors.Errorf("no argument given, requires exactly 1 argument")
+ }
+ term := args[0]
+
+ if err := validateFlags(c, searchFlags); err != nil {
+ return err
+ }
+
+ runtime, err := getRuntime(c)
+ if err != nil {
+ return errors.Wrapf(err, "could not get runtime")
+ }
+ defer runtime.Shutdown(false)
+
+ format := genSearchFormat(c.String("format"))
+ opts := searchOpts{
+ format: format,
+ noTrunc: c.Bool("no-trunc"),
+ limit: c.Int("limit"),
+ filter: c.StringSlice("filter"),
+ }
+
+ var registries []string
+ if len(c.StringSlice("registry")) > 0 {
+ registries = c.StringSlice("registry")
+ } else {
+ registries, err = libpod.GetRegistries()
+ if err != nil {
+ return errors.Wrapf(err, "error getting registries to search")
+ }
+ }
+
+ filter, err := parseSearchFilter(&opts)
+ if err != nil {
+ return err
+ }
+
+ return generateSearchOutput(term, registries, opts, *filter)
+}
+
+func genSearchFormat(format string) string {
+ if format != "" {
+ // "\t" from the command line is not being recognized as a tab
+ // replacing the string "\t" to a tab character if the user passes in "\t"
+ return strings.Replace(format, `\t`, "\t", -1)
+ }
+ return "table {{.Index}}\t{{.Name}}\t{{.Description}}\t{{.Stars}}\t{{.Official}}\t{{.Automated}}\t"
+}
+
+func searchToGeneric(params []searchParams) (genericParams []interface{}) {
+ for _, v := range params {
+ genericParams = append(genericParams, interface{}(v))
+ }
+ return genericParams
+}
+
+func (s *searchParams) headerMap() map[string]string {
+ v := reflect.Indirect(reflect.ValueOf(s))
+ values := make(map[string]string, v.NumField())
+
+ for i := 0; i < v.NumField(); i++ {
+ key := v.Type().Field(i).Name
+ value := key
+ values[key] = strings.ToUpper(splitCamelCase(value))
+ }
+ return values
+}
+
+func getSearchOutput(term string, registries []string, opts searchOpts, filter searchFilterParams) ([]searchParams, error) {
+ sc := common.GetSystemContext("", "", false)
+ // Max number of queries by default is 25
+ limit := maxQueries
+ if opts.limit != 0 {
+ limit = opts.limit
+ }
+
+ var paramsArr []searchParams
+ for _, reg := range registries {
+ results, err := docker.SearchRegistry(context.TODO(), sc, reg, term, limit)
+ if err != nil {
+ logrus.Errorf("error searching registry %q: %v", reg, err)
+ continue
+ }
+ index := reg
+ arr := strings.Split(reg, ".")
+ if len(arr) > 2 {
+ index = strings.Join(arr[len(arr)-2:], ".")
+ }
+
+ // limit is the number of results to output
+ // if the total number of results is less than the limit, output all
+ // if the limit has been set by the user, output those number of queries
+ limit := maxQueries
+ if len(results) < limit {
+ limit = len(results)
+ }
+ if opts.limit != 0 && opts.limit < len(results) {
+ limit = opts.limit
+ }
+
+ for i := 0; i < limit; i++ {
+ if len(opts.filter) > 0 {
+ // Check whether query matches filters
+ if !(matchesAutomatedFilter(filter, results[i]) && matchesOfficialFilter(filter, results[i]) && matchesStarFilter(filter, results[i])) {
+ continue
+ }
+ }
+ official := ""
+ if results[i].IsOfficial {
+ official = "[OK]"
+ }
+ automated := ""
+ if results[i].IsAutomated {
+ automated = "[OK]"
+ }
+ description := strings.Replace(results[i].Description, "\n", " ", -1)
+ if len(description) > 44 && !opts.noTrunc {
+ description = description[:descriptionTruncLength] + "..."
+ }
+ name := index + "/" + results[i].Name
+ if index == "docker.io" && !strings.Contains(results[i].Name, "/") {
+ name = index + "/library/" + results[i].Name
+ }
+ params := searchParams{
+ Index: index,
+ Name: name,
+ Description: description,
+ Official: official,
+ Automated: automated,
+ Stars: results[i].StarCount,
+ }
+ paramsArr = append(paramsArr, params)
+ }
+ }
+ return paramsArr, nil
+}
+
+func generateSearchOutput(term string, registries []string, opts searchOpts, filter searchFilterParams) error {
+ searchOutput, err := getSearchOutput(term, registries, opts, filter)
+ if err != nil {
+ return err
+ }
+ if len(searchOutput) == 0 {
+ return nil
+ }
+ out := formats.StdoutTemplateArray{Output: searchToGeneric(searchOutput), Template: opts.format, Fields: searchOutput[0].headerMap()}
+ return formats.Writer(out).Out()
+}
+
+func parseSearchFilter(opts *searchOpts) (*searchFilterParams, error) {
+ filterParams := &searchFilterParams{}
+ ptrTrue := true
+ ptrFalse := false
+ for _, filter := range opts.filter {
+ arr := strings.Split(filter, "=")
+ switch arr[0] {
+ case "stars":
+ if len(arr) < 2 {
+ return nil, errors.Errorf("invalid `stars` filter %q, should be stars=<value>", filter)
+ }
+ stars, err := strconv.Atoi(arr[1])
+ if err != nil {
+ return nil, errors.Wrapf(err, "incorrect value type for stars filter")
+ }
+ filterParams.stars = stars
+ break
+ case "is-automated":
+ if len(arr) == 2 && arr[1] == "false" {
+ filterParams.isAutomated = &ptrFalse
+ } else {
+ filterParams.isAutomated = &ptrTrue
+ }
+ break
+ case "is-official":
+ if len(arr) == 2 && arr[1] == "false" {
+ filterParams.isOfficial = &ptrFalse
+ } else {
+ filterParams.isOfficial = &ptrTrue
+ }
+ break
+ default:
+ return nil, errors.Errorf("invalid filter type %q", filter)
+ }
+ }
+ return filterParams, nil
+}
+
+func matchesStarFilter(filter searchFilterParams, result docker.SearchResult) bool {
+ return result.StarCount >= filter.stars
+}
+
+func matchesAutomatedFilter(filter searchFilterParams, result docker.SearchResult) bool {
+ if filter.isAutomated != nil {
+ return result.IsAutomated == *filter.isAutomated
+ }
+ return true
+}
+
+func matchesOfficialFilter(filter searchFilterParams, result docker.SearchResult) bool {
+ if filter.isOfficial != nil {
+ return result.IsOfficial == *filter.isOfficial
+ }
+ return true
+}
diff --git a/completions/bash/podman b/completions/bash/podman
index 0087c56b9..b1033df1c 100644
--- a/completions/bash/podman
+++ b/completions/bash/podman
@@ -954,6 +954,19 @@ _podman_pull() {
_complete_ "$options_with_args" "$boolean_options"
}
+_podman_search() {
+ local options_with_args="
+ --filter -f
+ --format
+ --limit
+ --registry
+ "
+ local boolean_options="
+ --no-trunc
+ "
+ _complete_ "$options_with_args" "$boolean_options"
+}
+
_podman_unmount() {
_podman_umount $@
}
@@ -1589,6 +1602,7 @@ _podman_podman() {
rmi
run
save
+ search
start
stats
stop
diff --git a/docs/podman-search.1.md b/docs/podman-search.1.md
new file mode 100644
index 000000000..668748d3f
--- /dev/null
+++ b/docs/podman-search.1.md
@@ -0,0 +1,115 @@
+% podman(1) podman-search - Tool to search registries for an image
+% Urvashi Mohnani
+# podman-search "1" "January 2018" "podman"
+
+## NAME
+podman search - Search a registry for an image
+
+## SYNOPSIS
+**podman search**
+**TERM**
+[**--filter**|**-f**]
+[**--format**]
+[**--limit**]
+[**--no-trunc**]
+[**--registry**]
+[**--help**|**-h**]
+
+## DESCRIPTION
+**podman search** searches a registry or a list of registries for a matching image.
+The user can specify which registry to search by setting the **--registry** flag, default
+is the default registries set in the config file - **/etc/containers/registries.conf**.
+The number of results can be limited using the **--limit** flag. If more than one registry
+is being searched, the limit will be applied to each registry. The output can be filtered
+using the **--filter** flag.
+
+**podman [GLOBAL OPTIONS]**
+
+**podman search [GLOBAL OPTIONS]**
+
+**podman search [OPTIONS] TERM**
+
+## OPTIONS
+
+**--filter, -f**
+Filter output based on conditions provided (default [])
+
+Supported filters are:
+- stars (int - number of stars the image has)
+- is-automated (boolean - true | false) - is the image automated or not
+- is-official (boolean - true | false) - is the image official or not
+
+**--format**
+Change the output format to a Go template
+
+Valid placeholders for the Go template are listed below:
+
+| **Placeholder** | **Description** |
+| --------------- | ---------------------------- |
+| .Index | Registry |
+| .Name | Image name |
+| .Descriptions | Image description |
+| .Stars | Star count of image |
+| .Official | "[OK]" if image is official |
+| .Automated | "[OK]" if image is automated |
+
+**--limit**
+Limit the number of results
+Note: The results from each registry will be limited to this value.
+Example if limit is 10 and two registries are being searched, the total
+number of results will be 20, 10 from each (if there are at least 10 matches in each).
+The order of the search results is the order in which the API endpoint returns the results.
+
+**--no-trunc**
+Do not truncate the output
+
+**--registry**
+Specific registry to search (only the given registry will be searched, not the default registries)
+
+## EXAMPLES
+
+```
+# podman search --limit 3 rhel
+INDEX NAME DESCRIPTION STARS OFFICIAL AUTOMATED
+docker.io docker.io/richxsl/rhel7 RHEL 7 image with minimal installation 9
+docker.io docker.io/bluedata/rhel7 RHEL-7.x base container images 1
+docker.io docker.io/gidikern/rhel-oracle-jre RHEL7 with jre8u60 5 [OK]
+redhat.com redhat.com/rhel This platform image provides a minimal runti... 0
+redhat.com redhat.com/rhel6 This platform image provides a minimal runti... 0
+redhat.com redhat.com/rhel6.5 This platform image provides a minimal runti... 0
+```
+
+```
+# podman search alpine
+INDEX NAME DESCRIPTION STARS OFFICIAL AUTOMATED
+docker.io docker.io/library/alpine A minimal Docker image based on Alpine Linux... 3009 [OK]
+docker.io docker.io/mhart/alpine-node Minimal Node.js built on Alpine Linux 332
+docker.io docker.io/anapsix/alpine-java Oracle Java 8 (and 7) with GLIBC 2.23 over A... 272 [OK]
+docker.io docker.io/tenstartups/alpine Alpine linux base docker image with useful p... 5 [OK]
+```
+
+```
+# podman search --registry registry.fedoraproject.org fedora
+INDEX NAME DESCRIPTION STARS OFFICIAL AUTOMATED
+fedoraproject.org fedoraproject.org/fedora 0
+fedoraproject.org fedoraproject.org/fedora-minimal 0
+```
+
+```
+# podman search --filter=is-official alpine
+INDEX NAME DESCRIPTION STARS OFFICIAL AUTOMATED
+docker.io docker.io/library/alpine A minimal Docker image based on Alpine Linux... 3009 [OK]
+```
+
+```
+# podman search --registry registry.fedoraproject.org --format "table {{.Index}} {{.Name}}" fedora
+INDEX NAME
+fedoraproject.org fedoraproject.org/fedora
+fedoraproject.org fedoraproject.org/fedora-minimal
+```
+
+## SEE ALSO
+podman(1), crio(8), crio.conf(5)
+
+## HISTORY
+January 2018, Originally compiled by Urvashi Mohnani <umohnani@redhat.com>
diff --git a/test/podman_search.bats b/test/podman_search.bats
new file mode 100644
index 000000000..07621d722
--- /dev/null
+++ b/test/podman_search.bats
@@ -0,0 +1,43 @@
+#!/usr/bin/env bats
+
+load helpers
+
+function teardown() {
+ cleanup_test
+}
+
+@test "podman search" {
+ run ${PODMAN_BINARY} ${PODMAN_OPTIONS} search alpine
+ echo "$output"
+ [ "$status" -eq 0 ]
+}
+
+@test "podman search registry flag" {
+ run ${PODMAN_BINARY} ${PODMAN_OPTIONS} search --registry registry.fedoraproject.org fedora
+ echo "$output"
+ [ "$status" -eq 0 ]
+}
+
+@test "podman search filter flag" {
+ run ${PODMAN_BINARY} ${PODMAN_OPTIONS} search --filter=is-official alpine
+ echo "$output"
+ [ "$status" -eq 0 ]
+}
+
+@test "podman search format flag" {
+ run ${PODMAN_BINARY} ${PODMAN_OPTIONS} search --format "table {{.Index}} {{.Name}}" alpine
+ echo "$output"
+ [ "$status" -eq 0 ]
+}
+
+@test "podman search no-trunc flag" {
+ run ${PODMAN_BINARY} ${PODMAN_OPTIONS} search --no-trunc alpine
+ echo "$output"
+ [ "$status" -eq 0 ]
+}
+
+@test "podman search limit flag" {
+ run ${PODMAN_BINARY} ${PODMAN_OPTIONS} search --limit 3 alpine
+ echo "$output"
+ [ "$status" -eq 0 ]
+} \ No newline at end of file
diff --git a/transfer.md b/transfer.md
index 30e277a86..53d6c472c 100644
--- a/transfer.md
+++ b/transfer.md
@@ -60,6 +60,7 @@ There are other equivalents for these tools
| `docker rmi` | [`podman rmi`](./docs/podman-rmi.1.md) |
| `docker run` | [`podman run`](./docs/podman-run.1.md) |
| `docker save` | [`podman save`](./docs/podman-save.1.md) |
+| `docker search` | [`podman search`](./docs/podman-search.1.md) |
| `docker start` | [`podman start`](./docs/podman-start.1.md) |
| `docker stop` | [`podman stop`](./docs/podman-stop.1.md) |
| `docker tag` | [`podman tag`](./docs/podman-tag.1.md) |
@@ -85,7 +86,6 @@ Those Docker commands currently do not have equivalents in `podman`:
| `docker port` ||
| `docker rename` | podman does not support rename, you need to use `podman rm` and `podman create` to rename a container.|
| `docker restart` | podman does not support restart. We recommend that you put your podman containers into a systemd unit file and use it for restarting applications.|
-| `docker search` ||
| `docker secret` ||
| `docker service` ||
| `docker stack` ||