mirror of
https://github.com/containers/podman.git
synced 2025-06-28 06:18:57 +08:00
Merge pull request #5709 from vrothberg/v2-search
podmanV2: implement search
This commit is contained in:
157
cmd/podmanV2/images/search.go
Normal file
157
cmd/podmanV2/images/search.go
Normal 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
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
@ -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() {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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"`
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
Reference in New Issue
Block a user