From 66798e993a1d3c111f15a85242cac1427c39f53e Mon Sep 17 00:00:00 2001 From: Qi Wang Date: Tue, 29 Sep 2020 16:40:10 -0400 Subject: Search repository tags using --list-tags For fix of BZ: https://bugzilla.redhat.com/show_bug.cgi?id=1684263 Add --list-tags to podman search to return a table the repository tags. Signed-off-by: Qi Wang --- cmd/podman/images/search.go | 12 ++++++++ completions/bash/podman | 1 + docs/source/markdown/podman-search.1.md | 18 ++++++++++++ libpod/image/search.go | 51 +++++++++++++++++++++++++++++++++ pkg/api/handlers/libpod/images.go | 7 +++-- pkg/api/server/register_images.go | 4 +++ pkg/bindings/images/images.go | 1 + pkg/domain/entities/images.go | 4 +++ pkg/domain/infra/abi/images.go | 2 ++ test/e2e/search_test.go | 20 +++++++++++++ 10 files changed, 118 insertions(+), 2 deletions(-) diff --git a/cmd/podman/images/search.go b/cmd/podman/images/search.go index b8f590585..8edd776ce 100644 --- a/cmd/podman/images/search.go +++ b/cmd/podman/images/search.go @@ -85,6 +85,7 @@ func searchFlags(flags *pflag.FlagSet) { flags.BoolVar(&searchOptions.NoTrunc, "no-trunc", false, "Do not truncate the output") flags.StringVar(&searchOptions.Authfile, "authfile", auth.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") + flags.BoolVar(&searchOptions.ListTags, "list-tags", false, "List the tags of the input registry") } // imageSearch implements the command for searching images. @@ -101,6 +102,10 @@ func imageSearch(cmd *cobra.Command, args []string) error { return errors.Errorf("Limit %d is outside the range of [1, 100]", searchOptions.Limit) } + 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 @@ -119,12 +124,19 @@ func imageSearch(cmd *cobra.Command, args []string) error { if err != nil { return err } + if len(searchReport) == 0 { return nil } hdrs := report.Headers(entities.ImageSearchReport{}, nil) row := "{{.Index}}\t{{.Name}}\t{{.Description}}\t{{.Stars}}\t{{.Official}}\t{{.Automated}}\n" + if searchOptions.ListTags { + if len(searchOptions.Filters) != 0 { + return errors.Errorf("filters are not applicable to list tags result") + } + row = "{{.Name}}\t{{.Tag}}\n" + } if cmd.Flags().Changed("format") { row = report.NormalizeFormat(searchOptions.Format) } diff --git a/completions/bash/podman b/completions/bash/podman index e12862126..564d35f67 100644 --- a/completions/bash/podman +++ b/completions/bash/podman @@ -2024,6 +2024,7 @@ _podman_search() { --help -h --no-trunc + --list-tags " _complete_ "$options_with_args" "$boolean_options" } diff --git a/docs/source/markdown/podman-search.1.md b/docs/source/markdown/podman-search.1.md index 2c2a8f012..fc09d96ea 100644 --- a/docs/source/markdown/podman-search.1.md +++ b/docs/source/markdown/podman-search.1.md @@ -56,6 +56,9 @@ Valid placeholders for the Go template are listed below: | .Stars | Star count of image | | .Official | "[OK]" if image is official | | .Automated | "[OK]" if image is automated | +| .Tag | Repository tag | + +Note: use .Tag only if the --list-tags is set. **--limit**=*limit* @@ -65,6 +68,12 @@ 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. +**--list-tags** + +List the available tags in the repository for the specified image. +**Note:** --list-tags requires the search term to be a fully specified image name. +The result contains the Image name and its tag, one line for every tag associated with the image. + **--no-trunc** Do not truncate the output @@ -140,6 +149,15 @@ fedoraproject.org registry.fedoraproject.org/f25/kubernetes-proxy fedoraproject.org registry.fedoraproject.org/f25/kubernetes-scheduler 0 fedoraproject.org registry.fedoraproject.org/f25/mariadb 0 ``` + +``` +$ podman search --list-tags registry.redhat.io/rhel +NAME TAG +registry.redhat.io/rhel 7.3-74 +registry.redhat.io/rhel 7.6-301 +registry.redhat.io/rhel 7.1-9 +... +``` Note: This works only with registries that implement the v2 API. If tried with a v1 registry an error will be returned. ## FILES diff --git a/libpod/image/search.go b/libpod/image/search.go index 6bcc6d3f8..5f5845989 100644 --- a/libpod/image/search.go +++ b/libpod/image/search.go @@ -2,11 +2,13 @@ package image import ( "context" + "fmt" "strconv" "strings" "sync" "github.com/containers/image/v5/docker" + "github.com/containers/image/v5/transports/alltransports" "github.com/containers/image/v5/types" sysreg "github.com/containers/podman/v2/pkg/registries" "github.com/pkg/errors" @@ -34,6 +36,8 @@ type SearchResult struct { Official string // Automated indicates if the image was created by an automated build. Automated string + // Tag is the image tag + Tag string } // SearchOptions are used to control the behaviour of SearchImages. @@ -49,6 +53,8 @@ type SearchOptions struct { Authfile string // InsecureSkipTLSVerify allows to skip TLS verification. InsecureSkipTLSVerify types.OptionalBool + // ListTags returns the search result with available tags + ListTags bool } // SearchFilter allows filtering the results of SearchImages. @@ -147,6 +153,15 @@ func searchImageInRegistry(term string, registry string, options SearchOptions) // every types.SystemContext, and to compute the value just once in one // place. sc.SystemRegistriesConfPath = sysreg.SystemRegistriesConfPath() + if options.ListTags { + results, err := searchRepositoryTags(registry, term, sc, options) + if err != nil { + logrus.Errorf("error listing registry tags %q: %v", registry, err) + return []SearchResult{} + } + return results + } + results, err := docker.SearchRegistry(context.TODO(), sc, registry, term, limit) if err != nil { logrus.Errorf("error searching registry %q: %v", registry, err) @@ -207,6 +222,42 @@ func searchImageInRegistry(term string, registry string, options SearchOptions) return paramsArr } +func searchRepositoryTags(registry, term string, sc *types.SystemContext, options SearchOptions) ([]SearchResult, error) { + dockerPrefix := fmt.Sprintf("%s://", docker.Transport.Name()) + imageRef, err := alltransports.ParseImageName(fmt.Sprintf("%s/%s", registry, term)) + if err == nil && imageRef.Transport().Name() != docker.Transport.Name() { + return nil, errors.Errorf("reference %q must be a docker reference", term) + } else if err != nil { + imageRef, err = alltransports.ParseImageName(fmt.Sprintf("%s%s", dockerPrefix, fmt.Sprintf("%s/%s", registry, term))) + if err != nil { + return nil, errors.Errorf("reference %q must be a docker reference", term) + } + } + tags, err := docker.GetRepositoryTags(context.TODO(), sc, imageRef) + if err != nil { + return nil, errors.Errorf("error getting repository tags: %v", err) + } + limit := maxQueries + if len(tags) < limit { + limit = len(tags) + } + if options.Limit != 0 { + limit = len(tags) + if options.Limit < limit { + limit = options.Limit + } + } + paramsArr := []SearchResult{} + for i := 0; i < limit; i++ { + params := SearchResult{ + Name: imageRef.DockerReference().Name(), + Tag: tags[i], + } + paramsArr = append(paramsArr, params) + } + return paramsArr, nil +} + // ParseSearchFilter turns the filter into a SearchFilter that can be used for // searching images. func ParseSearchFilter(filter []string) (*SearchFilter, error) { diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go index 43123c5a3..1292090fb 100644 --- a/pkg/api/handlers/libpod/images.go +++ b/pkg/api/handlers/libpod/images.go @@ -608,6 +608,7 @@ func SearchImages(w http.ResponseWriter, r *http.Request) { NoTrunc bool `json:"noTrunc"` Filters []string `json:"filters"` TLSVerify bool `json:"tlsVerify"` + ListTags bool `json:"listTags"` }{ // This is where you can override the golang default value for one of fields } @@ -618,8 +619,9 @@ func SearchImages(w http.ResponseWriter, r *http.Request) { } options := image.SearchOptions{ - Limit: query.Limit, - NoTrunc: query.NoTrunc, + Limit: query.Limit, + NoTrunc: query.NoTrunc, + ListTags: query.ListTags, } if _, found := r.URL.Query()["tlsVerify"]; found { options.InsecureSkipTLSVerify = types.NewOptionalBool(!query.TLSVerify) @@ -650,6 +652,7 @@ func SearchImages(w http.ResponseWriter, r *http.Request) { reports[i].Stars = searchResults[i].Stars reports[i].Official = searchResults[i].Official reports[i].Automated = searchResults[i].Automated + reports[i].Tag = searchResults[i].Tag } utils.WriteResponse(w, http.StatusOK, reports) diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go index ad779203d..c2423218a 100644 --- a/pkg/api/server/register_images.go +++ b/pkg/api/server/register_images.go @@ -169,6 +169,10 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // - `is-automated=(true|false)` // - `is-official=(true|false)` // - `stars=` Matches images that has at least 'number' stars. + // - in: query + // name: listTags + // type: boolean + // description: list the available tags in the repository // produces: // - application/json // responses: diff --git a/pkg/bindings/images/images.go b/pkg/bindings/images/images.go index a78e7f4c6..2d3035d8d 100644 --- a/pkg/bindings/images/images.go +++ b/pkg/bindings/images/images.go @@ -314,6 +314,7 @@ func Search(ctx context.Context, term string, opts entities.ImageSearchOptions) params.Set("term", term) params.Set("limit", strconv.Itoa(opts.Limit)) params.Set("noTrunc", strconv.FormatBool(opts.NoTrunc)) + params.Set("listTags", strconv.FormatBool(opts.ListTags)) for _, f := range opts.Filters { params.Set("filters", f) } diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go index ac81c282d..982fa0cc0 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -214,6 +214,8 @@ type ImageSearchOptions struct { NoTrunc bool // SkipTLSVerify to skip HTTPS and certificate verification. SkipTLSVerify types.OptionalBool + // ListTags search the available tags of the repository + ListTags bool } // ImageSearchReport is the response from searching images. @@ -230,6 +232,8 @@ type ImageSearchReport struct { Official string // Automated indicates if the image was created by an automated build. Automated string + // Tag is the repository tag + Tag string } // Image List Options diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index 3bb7de83c..f9d733c63 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -511,6 +511,7 @@ func (ir *ImageEngine) Search(ctx context.Context, term string, opts entities.Im Limit: opts.Limit, NoTrunc: opts.NoTrunc, InsecureSkipTLSVerify: opts.SkipTLSVerify, + ListTags: opts.ListTags, } searchResults, err := image.SearchImages(term, searchOpts) @@ -529,6 +530,7 @@ func (ir *ImageEngine) Search(ctx context.Context, term string, opts entities.Im reports[i].Stars = searchResults[i].Stars reports[i].Official = searchResults[i].Official reports[i].Automated = searchResults[i].Automated + reports[i].Tag = searchResults[i].Tag } return reports, nil diff --git a/test/e2e/search_test.go b/test/e2e/search_test.go index 043da9059..0cf005529 100644 --- a/test/e2e/search_test.go +++ b/test/e2e/search_test.go @@ -423,4 +423,24 @@ registries = ['{{.Host}}:{{.Port}}']` Expect(search.ExitCode()).To(Equal(0)) Expect(len(search.OutputToStringArray()) > 1).To(BeTrue()) }) + + It("podman search repository tags", func() { + search := podmanTest.Podman([]string{"search", "--list-tags", "--limit", "30", "docker.io/library/alpine"}) + search.WaitWithDefaultTimeout() + Expect(search.ExitCode()).To(Equal(0)) + Expect(len(search.OutputToStringArray())).To(Equal(31)) + + search = podmanTest.Podman([]string{"search", "--list-tags", "docker.io/library/alpine"}) + search.WaitWithDefaultTimeout() + Expect(search.ExitCode()).To(Equal(0)) + Expect(len(search.OutputToStringArray()) > 2).To(BeTrue()) + + search = podmanTest.Podman([]string{"search", "--filter=is-official", "--list-tags", "docker.io/library/alpine"}) + search.WaitWithDefaultTimeout() + Expect(search.ExitCode()).To(Not(Equal(0))) + + search = podmanTest.Podman([]string{"search", "--list-tags", "docker.io/library/"}) + search.WaitWithDefaultTimeout() + Expect(len(search.OutputToStringArray()) == 0).To(BeTrue()) + }) }) -- cgit v1.2.3-54-g00ecf