automation-tests/cmd/podman/images/search.go

219 lines
7.0 KiB
Go

package images
import (
"fmt"
"os"
"strings"
"github.com/containers/common/pkg/auth"
"github.com/containers/common/pkg/completion"
"github.com/containers/common/pkg/report"
"github.com/containers/image/v5/types"
"github.com/containers/podman/v4/cmd/podman/registry"
"github.com/containers/podman/v4/pkg/domain/entities"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
// searchOptionsWrapper wraps entities.ImagePullOptions and prevents leaking
// CLI-only fields into the API types.
type searchOptionsWrapper struct {
entities.ImageSearchOptions
// CLI only flags
Compatible bool // Docker compat
TLSVerifyCLI bool // Used to convert to an optional bool later
Format string // For go templating
}
// listEntryTag is a utility structure used for json serialization.
type listEntryTag struct {
Name string
Tags []string
}
var (
searchOptions = searchOptionsWrapper{}
searchDescription = `Search registries for a given image. Can search all the default registries or a specific registry.
Users can limit the number of results, and filter the output based on certain conditions.`
searchCmd = &cobra.Command{
Use: "search [options] TERM",
Short: "Search registry for image",
Long: searchDescription,
RunE: imageSearch,
Args: cobra.ExactArgs(1),
ValidArgsFunction: completion.AutocompleteNone,
Example: `podman search --filter=is-official --limit 3 alpine
podman search registry.fedoraproject.org/ # only works with v2 registries
podman search --format "table {{.Index}} {{.Name}}" registry.fedoraproject.org/fedora`,
}
imageSearchCmd = &cobra.Command{
Use: searchCmd.Use,
Short: searchCmd.Short,
Long: searchCmd.Long,
RunE: searchCmd.RunE,
Args: searchCmd.Args,
ValidArgsFunction: searchCmd.ValidArgsFunction,
Example: `podman image search --filter=is-official --limit 3 alpine
podman image search registry.fedoraproject.org/ # only works with v2 registries
podman image search --format "table {{.Index}} {{.Name}}" registry.fedoraproject.org/fedora`,
}
)
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: searchCmd,
})
searchFlags(searchCmd)
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: imageSearchCmd,
Parent: imageCmd,
})
searchFlags(imageSearchCmd)
}
// searchFlags set the flags for the pull command.
func searchFlags(cmd *cobra.Command) {
flags := cmd.Flags()
filterFlagName := "filter"
flags.StringSliceVarP(&searchOptions.Filters, filterFlagName, "f", []string{}, "Filter output based on conditions provided (default [])")
// TODO add custom filter function
_ = cmd.RegisterFlagCompletionFunc(filterFlagName, completion.AutocompleteNone)
formatFlagName := "format"
flags.StringVar(&searchOptions.Format, formatFlagName, "", "Change the output format to JSON or a Go template")
_ = cmd.RegisterFlagCompletionFunc(formatFlagName, completion.AutocompleteNone)
limitFlagName := "limit"
flags.IntVar(&searchOptions.Limit, limitFlagName, 0, "Limit the number of results")
_ = cmd.RegisterFlagCompletionFunc(limitFlagName, completion.AutocompleteNone)
flags.Bool("no-trunc", true, "Do not truncate the output. Default: true")
flags.BoolVar(&searchOptions.Compatible, "compatible", false, "List stars, official and automated columns (Docker compatibility)")
authfileFlagName := "authfile"
flags.StringVar(&searchOptions.Authfile, authfileFlagName, auth.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override")
_ = cmd.RegisterFlagCompletionFunc(authfileFlagName, completion.AutocompleteDefault)
flags.BoolVar(&searchOptions.TLSVerifyCLI, "tls-verify", true, "Require HTTPS and verify certificates when contacting registries")
flags.BoolVar(&searchOptions.ListTags, "list-tags", false, "List the tags of the input registry")
}
// imageSearch implements the command for searching images.
func imageSearch(cmd *cobra.Command, args []string) error {
searchTerm := ""
switch len(args) {
case 1:
searchTerm = args[0]
default:
return errors.Errorf("search requires exactly one argument")
}
if searchOptions.ListTags && len(searchOptions.Filters) != 0 {
return errors.Errorf("filters are not applicable to list tags result")
}
// TLS verification in c/image is controlled via a `types.OptionalBool`
// which allows for distinguishing among set-true, set-false, unspecified
// which is important to implement a sane way of dealing with defaults of
// boolean CLI flags.
if cmd.Flags().Changed("tls-verify") {
searchOptions.SkipTLSVerify = types.NewOptionalBool(!searchOptions.TLSVerifyCLI)
}
if searchOptions.Authfile != "" {
if _, err := os.Stat(searchOptions.Authfile); err != nil {
return err
}
}
searchReport, err := registry.ImageEngine().Search(registry.GetContext(), searchTerm, searchOptions.ImageSearchOptions)
if err != nil {
return err
}
if len(searchReport) == 0 {
return nil
}
noTrunc, _ := cmd.Flags().GetBool("no-trunc")
isJSON := report.IsJSON(searchOptions.Format)
for i, element := range searchReport {
d := strings.ReplaceAll(element.Description, "\n", " ")
if len(d) > 44 && !(noTrunc || isJSON) {
d = strings.TrimSpace(d[:44]) + "..."
}
searchReport[i].Description = d
}
rpt := report.New(os.Stdout, cmd.Name())
defer rpt.Flush()
switch {
case searchOptions.ListTags:
if len(searchOptions.Filters) != 0 {
return errors.Errorf("filters are not applicable to list tags result")
}
if isJSON {
listTagsEntries := buildListTagsJSON(searchReport)
return printArbitraryJSON(listTagsEntries)
}
rpt, err = rpt.Parse(report.OriginPodman, "{{range .}}{{.Name}}\t{{.Tag}}\n{{end -}}")
case isJSON:
return printArbitraryJSON(searchReport)
case cmd.Flags().Changed("format"):
rpt, err = rpt.Parse(report.OriginUser, searchOptions.Format)
default:
row := "{{.Name}}\t{{.Description}}"
if searchOptions.Compatible {
row += "\t{{.Stars}}\t{{.Official}}\t{{.Automated}}"
}
row = "{{range . }}" + row + "\n{{end -}}"
rpt, err = rpt.Parse(report.OriginPodman, row)
}
if err != nil {
return err
}
if rpt.RenderHeaders {
hdrs := report.Headers(entities.ImageSearchReport{}, nil)
if err := rpt.Execute(hdrs); err != nil {
return errors.Wrapf(err, "failed to write report column headers")
}
}
return rpt.Execute(searchReport)
}
func printArbitraryJSON(v interface{}) error {
prettyJSON, err := json.MarshalIndent(v, "", " ")
if err != nil {
return err
}
fmt.Println(string(prettyJSON))
return nil
}
func buildListTagsJSON(searchReport []entities.ImageSearchReport) []listEntryTag {
entries := make([]listEntryTag, 0)
ReportLoop:
for _, report := range searchReport {
for idx, entry := range entries {
if entry.Name == report.Name {
entries[idx].Tags = append(entries[idx].Tags, report.Tag)
continue ReportLoop
}
}
newElem := listEntryTag{
report.Name,
[]string{report.Tag},
}
entries = append(entries, newElem)
}
return entries
}