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
This commit is contained in:
umohnani8
2018-01-10 09:35:23 -05:00
committed by Atomic Bot
parent 1a48a7a7c0
commit 0d7e6fa22f
6 changed files with 464 additions and 1 deletions

View File

@ -64,6 +64,7 @@ func main() {
rmiCommand,
runCommand,
saveCommand,
searchCommand,
startCommand,
statsCommand,
stopCommand,

290
cmd/podman/search.go Normal file
View File

@ -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
}

View File

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

115
docs/podman-search.1.md Normal file
View File

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

43
test/podman_search.bats Normal file
View File

@ -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 ]
}

View File

@ -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` ||