Merge pull request #5709 from vrothberg/v2-search

podmanV2: implement search
This commit is contained in:
OpenShift Merge Robot
2020-04-14 14:51:39 +02:00
committed by GitHub
11 changed files with 324 additions and 55 deletions

View File

@ -0,0 +1,157 @@
package images
import (
"reflect"
"strings"
buildahcli "github.com/containers/buildah/pkg/cli"
"github.com/containers/buildah/pkg/formats"
"github.com/containers/image/v5/types"
"github.com/containers/libpod/cmd/podmanV2/registry"
"github.com/containers/libpod/pkg/domain/entities"
"github.com/containers/libpod/pkg/util/camelcase"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// searchOptionsWrapper wraps entities.ImagePullOptions and prevents leaking
// CLI-only fields into the API types.
type searchOptionsWrapper struct {
entities.ImageSearchOptions
// CLI only flags
TLSVerifyCLI bool // Used to convert to an optional bool later
Format string // For go templating
}
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.`
// Command: podman search
searchCmd = &cobra.Command{
Use: "search [flags] TERM",
Short: "Search registry for image",
Long: searchDescription,
PreRunE: preRunE,
RunE: imageSearch,
Args: cobra.ExactArgs(1),
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`,
}
// Command: podman image search
imageSearchCmd = &cobra.Command{
Use: searchCmd.Use,
Short: searchCmd.Short,
Long: searchCmd.Long,
PreRunE: searchCmd.PreRunE,
RunE: searchCmd.RunE,
Args: searchCmd.Args,
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() {
// search
registry.Commands = append(registry.Commands, registry.CliCommand{
Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode},
Command: searchCmd,
})
searchCmd.SetHelpTemplate(registry.HelpTemplate())
searchCmd.SetUsageTemplate(registry.UsageTemplate())
flags := searchCmd.Flags()
searchFlags(flags)
// images search
registry.Commands = append(registry.Commands, registry.CliCommand{
Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode},
Command: imageSearchCmd,
Parent: imageCmd,
})
imageSearchFlags := imageSearchCmd.Flags()
searchFlags(imageSearchFlags)
}
// searchFlags set the flags for the pull command.
func searchFlags(flags *pflag.FlagSet) {
flags.StringSliceVarP(&searchOptions.Filters, "filter", "f", []string{}, "Filter output based on conditions provided (default [])")
flags.StringVar(&searchOptions.Format, "format", "", "Change the output format to a Go template")
flags.IntVar(&searchOptions.Limit, "limit", 0, "Limit the number of results")
flags.BoolVar(&searchOptions.NoTrunc, "no-trunc", false, "Do not truncate the output")
flags.StringVar(&searchOptions.Authfile, "authfile", buildahcli.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override")
flags.BoolVar(&searchOptions.TLSVerifyCLI, "tls-verify", true, "Require HTTPS and verify certificates when contacting registries")
if registry.IsRemote() {
_ = flags.MarkHidden("authfile")
_ = flags.MarkHidden("tls-verify")
}
}
// 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")
}
sarchOptsAPI := searchOptions.ImageSearchOptions
// 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") {
sarchOptsAPI.TLSVerify = types.NewOptionalBool(pullOptions.TLSVerifyCLI)
}
searchReport, err := registry.ImageEngine().Search(registry.GetContext(), searchTerm, sarchOptsAPI)
if err != nil {
return err
}
format := genSearchFormat(searchOptions.Format)
if len(searchReport) == 0 {
return nil
}
out := formats.StdoutTemplateArray{Output: searchToGeneric(searchReport), Template: format, Fields: searchHeaderMap()}
return out.Out()
}
// searchHeaderMap returns the headers of a SearchResult.
func searchHeaderMap() map[string]string {
s := new(entities.ImageSearchReport)
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(strings.Join(camelcase.Split(value), " "))
}
return values
}
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 []entities.ImageSearchReport) (genericParams []interface{}) {
for _, v := range params {
genericParams = append(genericParams, interface{}(v))
}
return genericParams
}

View File

@ -57,6 +57,7 @@ func SearchImages(w http.ResponseWriter, r *http.Request) {
Filter: filter,
Limit: query.Limit,
}
results, err := image.SearchImages(query.Term, options)
if err != nil {
utils.BadRequest(w, "term", query.Term, err)

View File

@ -645,3 +645,56 @@ func UntagImage(w http.ResponseWriter, r *http.Request) {
}
utils.WriteResponse(w, http.StatusCreated, "")
}
func SearchImages(w http.ResponseWriter, r *http.Request) {
decoder := r.Context().Value("decoder").(*schema.Decoder)
query := struct {
Term string `json:"term"`
Limit int `json:"limit"`
Filters []string `json:"filters"`
TLSVerify bool `json:"tlsVerify"`
}{
// This is where you can override the golang default value for one of fields
}
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse parameters for %s", r.URL.String()))
return
}
options := image.SearchOptions{
Limit: query.Limit,
}
if _, found := r.URL.Query()["tlsVerify"]; found {
options.InsecureSkipTLSVerify = types.NewOptionalBool(!query.TLSVerify)
}
if _, found := r.URL.Query()["filters"]; found {
filter, err := image.ParseSearchFilter(query.Filters)
if err != nil {
utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Failed to parse filters parameter for %s", r.URL.String()))
return
}
options.Filter = *filter
}
searchResults, err := image.SearchImages(query.Term, options)
if err != nil {
utils.BadRequest(w, "term", query.Term, err)
return
}
// Convert from image.SearchResults to entities.ImageSearchReport. We don't
// want to leak any low-level packages into the remote client, which
// requires converting.
reports := make([]entities.ImageSearchReport, len(searchResults))
for i := range searchResults {
reports[i].Index = searchResults[i].Index
reports[i].Name = searchResults[i].Name
reports[i].Description = searchResults[i].Index
reports[i].Stars = searchResults[i].Stars
reports[i].Official = searchResults[i].Official
reports[i].Automated = searchResults[i].Automated
}
utils.WriteResponse(w, http.StatusOK, reports)
}

View File

@ -919,7 +919,7 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error {
// $ref: "#/responses/DocsSearchResponse"
// 500:
// $ref: '#/responses/InternalError'
r.Handle(VersionedPath("/libpod/images/search"), s.APIHandler(compat.SearchImages)).Methods(http.MethodGet)
r.Handle(VersionedPath("/libpod/images/search"), s.APIHandler(libpod.SearchImages)).Methods(http.MethodGet)
// swagger:operation DELETE /libpod/images/{name:.*} libpod libpodRemoveImage
// ---
// tags:

View File

@ -2,7 +2,6 @@ package images
import (
"context"
"errors"
"fmt"
"io"
"net/http"
@ -13,6 +12,7 @@ import (
"github.com/containers/libpod/pkg/api/handlers"
"github.com/containers/libpod/pkg/bindings"
"github.com/containers/libpod/pkg/domain/entities"
"github.com/pkg/errors"
)
// Exists a lightweight way to determine if an image exists in local storage. It returns a
@ -308,3 +308,34 @@ func Push(ctx context.Context, source string, destination string, options entiti
_, err = conn.DoRequest(nil, http.MethodPost, path, params)
return err
}
// Search is the binding for libpod's v2 endpoints for Search images.
func Search(ctx context.Context, term string, opts entities.ImageSearchOptions) ([]entities.ImageSearchReport, error) {
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
params := url.Values{}
params.Set("term", term)
params.Set("limit", strconv.Itoa(opts.Limit))
for _, f := range opts.Filters {
params.Set("filters", f)
}
if opts.TLSVerify != types.OptionalBoolUndefined {
val := bool(opts.TLSVerify == types.OptionalBoolTrue)
params.Set("tlsVerify", strconv.FormatBool(val))
}
response, err := conn.DoRequest(nil, http.MethodGet, "/images/search", params)
if err != nil {
return nil, err
}
results := []entities.ImageSearchReport{}
if err := response.Process(&results); err != nil {
return nil, err
}
return results, nil
}

View File

@ -1,41 +0,0 @@
package images
import (
"context"
"net/http"
"net/url"
"strconv"
"github.com/containers/libpod/libpod/image"
"github.com/containers/libpod/pkg/bindings"
)
// Search looks for the given image (term) in container image registries. The optional limit parameter sets
// a maximum number of results returned. The optional filters parameter allow for more specific image
// searches.
func Search(ctx context.Context, term string, limit *int, filters map[string][]string) ([]image.SearchResult, error) {
var (
searchResults []image.SearchResult
)
conn, err := bindings.GetClient(ctx)
if err != nil {
return nil, err
}
params := url.Values{}
params.Set("term", term)
if limit != nil {
params.Set("limit", strconv.Itoa(*limit))
}
if filters != nil {
stringFilter, err := bindings.FiltersToString(filters)
if err != nil {
return nil, err
}
params.Set("filters", stringFilter)
}
response, err := conn.DoRequest(nil, http.MethodGet, "/images/search", params)
if err != nil {
return searchResults, nil
}
return searchResults, response.Process(&searchResults)
}

View File

@ -314,11 +314,11 @@ var _ = Describe("Podman images", func() {
})
It("Search for an image", func() {
imgs, err := images.Search(bt.conn, "alpine", nil, nil)
reports, err := images.Search(bt.conn, "alpine", entities.ImageSearchOptions{})
Expect(err).To(BeNil())
Expect(len(imgs)).To(BeNumerically(">", 1))
Expect(len(reports)).To(BeNumerically(">", 1))
var foundAlpine bool
for _, i := range imgs {
for _, i := range reports {
if i.Name == "docker.io/library/alpine" {
foundAlpine = true
break
@ -327,23 +327,20 @@ var _ = Describe("Podman images", func() {
Expect(foundAlpine).To(BeTrue())
// Search for alpine with a limit of 10
ten := 10
imgs, err = images.Search(bt.conn, "docker.io/alpine", &ten, nil)
reports, err = images.Search(bt.conn, "docker.io/alpine", entities.ImageSearchOptions{Limit: 10})
Expect(err).To(BeNil())
Expect(len(imgs)).To(BeNumerically("<=", 10))
Expect(len(reports)).To(BeNumerically("<=", 10))
// Search for alpine with stars greater than 100
filters := make(map[string][]string)
filters["stars"] = []string{"100"}
imgs, err = images.Search(bt.conn, "docker.io/alpine", nil, filters)
reports, err = images.Search(bt.conn, "docker.io/alpine", entities.ImageSearchOptions{Filters: []string{"stars=100"}})
Expect(err).To(BeNil())
for _, i := range imgs {
for _, i := range reports {
Expect(i.Stars).To(BeNumerically(">=", 100))
}
// Search with a fqdn
imgs, err = images.Search(bt.conn, "quay.io/libpod/alpine_nginx", nil, nil)
Expect(len(imgs)).To(BeNumerically(">=", 1))
reports, err = images.Search(bt.conn, "quay.io/libpod/alpine_nginx", entities.ImageSearchOptions{})
Expect(len(reports)).To(BeNumerically(">=", 1))
})
It("Prune images", func() {

View File

@ -19,4 +19,5 @@ type ImageEngine interface {
Save(ctx context.Context, nameOrId string, tags []string, options ImageSaveOptions) error
Tag(ctx context.Context, nameOrId string, tags []string, options ImageTagOptions) error
Untag(ctx context.Context, nameOrId string, tags []string, options ImageUntagOptions) error
Search(ctx context.Context, term string, opts ImageSearchOptions) ([]ImageSearchReport, error)
}

View File

@ -181,6 +181,37 @@ type ImagePushOptions struct {
TLSVerify types.OptionalBool
}
// ImageSearchOptions are the arguments for searching images.
type ImageSearchOptions struct {
// Authfile is the path to the authentication file. Ignored for remote
// calls.
Authfile string
// Filters for the search results.
Filters []string
// Limit the number of results.
Limit int
// NoTrunc will not truncate the output.
NoTrunc bool
// TLSVerify to enable/disable HTTPS and certificate verification.
TLSVerify types.OptionalBool
}
// ImageSearchReport is the response from searching images.
type ImageSearchReport struct {
// Index is the image index (e.g., "docker.io" or "quay.io")
Index string
// Name is the canoncical name of the image (e.g., "docker.io/library/alpine").
Name string
// Description of the image.
Description string
// Stars is the number of stars of the image.
Stars int
// Official indicates if it's an official image.
Official string
// Automated indicates if the image was created by an automated build.
Automated string
}
type ImageListOptions struct {
All bool `json:"all" schema:"all"`
Filter []string `json:"Filter,omitempty"`

View File

@ -425,3 +425,38 @@ func (ir *ImageEngine) Diff(_ context.Context, nameOrId string, _ entities.DiffO
}
return &entities.DiffReport{Changes: changes}, nil
}
func (ir *ImageEngine) Search(ctx context.Context, term string, opts entities.ImageSearchOptions) ([]entities.ImageSearchReport, error) {
filter, err := image.ParseSearchFilter(opts.Filters)
if err != nil {
return nil, err
}
searchOpts := image.SearchOptions{
Authfile: opts.Authfile,
Filter: *filter,
Limit: opts.Limit,
NoTrunc: opts.NoTrunc,
InsecureSkipTLSVerify: opts.TLSVerify,
}
searchResults, err := image.SearchImages(term, searchOpts)
if err != nil {
return nil, err
}
// Convert from image.SearchResults to entities.ImageSearchReport. We don't
// want to leak any low-level packages into the remote client, which
// requires converting.
reports := make([]entities.ImageSearchReport, len(searchResults))
for i := range searchResults {
reports[i].Index = searchResults[i].Index
reports[i].Name = searchResults[i].Name
reports[i].Description = searchResults[i].Index
reports[i].Stars = searchResults[i].Stars
reports[i].Official = searchResults[i].Official
reports[i].Automated = searchResults[i].Automated
}
return reports, nil
}

View File

@ -250,3 +250,7 @@ func (ir *ImageEngine) Diff(ctx context.Context, nameOrId string, _ entities.Dif
}
return &entities.DiffReport{Changes: changes}, nil
}
func (ir *ImageEngine) Search(ctx context.Context, term string, opts entities.ImageSearchOptions) ([]entities.ImageSearchReport, error) {
return images.Search(ir.ClientCxt, term, opts)
}