mirror of
https://github.com/containers/podman.git
synced 2025-07-02 00:30:00 +08:00
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:
@ -64,6 +64,7 @@ func main() {
|
||||
rmiCommand,
|
||||
runCommand,
|
||||
saveCommand,
|
||||
searchCommand,
|
||||
startCommand,
|
||||
statsCommand,
|
||||
stopCommand,
|
||||
|
290
cmd/podman/search.go
Normal file
290
cmd/podman/search.go
Normal 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
|
||||
}
|
@ -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
115
docs/podman-search.1.md
Normal 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
43
test/podman_search.bats
Normal 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 ]
|
||||
}
|
@ -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` ||
|
||||
|
Reference in New Issue
Block a user