diff --git a/cmd/podman/images/search.go b/cmd/podman/images/search.go index eb876d3d41..5e41ee1853 100644 --- a/cmd/podman/images/search.go +++ b/cmd/podman/images/search.go @@ -13,6 +13,7 @@ import ( "github.com/containers/podman/v4/cmd/podman/common" "github.com/containers/podman/v4/cmd/podman/registry" "github.com/containers/podman/v4/pkg/domain/entities" + "github.com/containers/podman/v4/pkg/util" "github.com/spf13/cobra" ) @@ -21,10 +22,11 @@ import ( 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 - NoTrunc bool + Compatible bool // Docker compat + CredentialsCLI string + TLSVerifyCLI bool // Used to convert to an optional bool later + Format string // For go templating + NoTrunc bool } // listEntryTag is a utility structure used for json serialization. @@ -100,8 +102,18 @@ func searchFlags(cmd *cobra.Command) { 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) + credsFlagName := "creds" + flags.StringVar(&searchOptions.CredentialsCLI, credsFlagName, "", "`Credentials` (USERNAME:PASSWORD) to use for authenticating to a registry") + _ = cmd.RegisterFlagCompletionFunc(credsFlagName, completion.AutocompleteNone) + 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") + + if !registry.IsRemote() { + certDirFlagName := "cert-dir" + flags.StringVar(&searchOptions.CertDir, certDirFlagName, "", "`Pathname` of a directory containing TLS certificates and keys") + _ = cmd.RegisterFlagCompletionFunc(certDirFlagName, completion.AutocompleteDefault) + } } // imageSearch implements the command for searching images. @@ -132,6 +144,15 @@ func imageSearch(cmd *cobra.Command, args []string) error { } } + if searchOptions.CredentialsCLI != "" { + creds, err := util.ParseRegistryCreds(searchOptions.CredentialsCLI) + if err != nil { + return err + } + searchOptions.Username = creds.Username + searchOptions.Password = creds.Password + } + searchReport, err := registry.ImageEngine().Search(registry.GetContext(), searchTerm, searchOptions.ImageSearchOptions) if err != nil { return err diff --git a/docs/source/markdown/options/cert-dir.md b/docs/source/markdown/options/cert-dir.md index ef780cc0b8..65192b306d 100644 --- a/docs/source/markdown/options/cert-dir.md +++ b/docs/source/markdown/options/cert-dir.md @@ -1,5 +1,5 @@ ####> This option file is used in: -####> podman build, container runlabel, image sign, kube play, login, manifest add, manifest push, pull, push +####> podman build, container runlabel, image sign, kube play, login, manifest add, manifest push, pull, push, search ####> If file is edited, make sure the changes ####> are applicable to all of those. #### **--cert-dir**=*path* diff --git a/docs/source/markdown/options/creds.md b/docs/source/markdown/options/creds.md index 05f111c5ca..9e425264c9 100644 --- a/docs/source/markdown/options/creds.md +++ b/docs/source/markdown/options/creds.md @@ -1,5 +1,5 @@ ####> This option file is used in: -####> podman build, container runlabel, kube play, manifest add, manifest push, pull, push +####> podman build, container runlabel, kube play, manifest add, manifest push, pull, push, search ####> If file is edited, make sure the changes ####> are applicable to all of those. #### **--creds**=*[username[:password]]* diff --git a/docs/source/markdown/podman-search.1.md.in b/docs/source/markdown/podman-search.1.md.in index 61926ad154..8fcc8239ed 100644 --- a/docs/source/markdown/podman-search.1.md.in +++ b/docs/source/markdown/podman-search.1.md.in @@ -32,11 +32,15 @@ Further note that searching without a search term will only work for registries @@option authfile +@@option cert-dir + #### **--compatible** After the name and the description, also show the stars, official and automated descriptors as Docker does. Podman does not show these descriptors by default since they are not supported by most public container registries. +@@option creds + #### **--filter**, **-f**=*filter* Filter output based on conditions provided (default []) diff --git a/pkg/api/handlers/compat/images_search.go b/pkg/api/handlers/compat/images_search.go index 2fc95e84ed..efd06fbf0d 100644 --- a/pkg/api/handlers/compat/images_search.go +++ b/pkg/api/handlers/compat/images_search.go @@ -34,23 +34,33 @@ func SearchImages(w http.ResponseWriter, r *http.Request) { return } - _, authfile, err := auth.GetCredentials(r) + authconf, authfile, err := auth.GetCredentials(r) if err != nil { utils.Error(w, http.StatusBadRequest, err) return } defer auth.RemoveAuthfile(authfile) + var username, password, idToken string + if authconf != nil { + username = authconf.Username + password = authconf.Password + idToken = authconf.IdentityToken + } + filters := []string{} for key, val := range query.Filters { filters = append(filters, fmt.Sprintf("%s=%s", key, val[0])) } options := entities.ImageSearchOptions{ - Authfile: authfile, - Limit: query.Limit, - ListTags: query.ListTags, - Filters: filters, + Authfile: authfile, + Limit: query.Limit, + ListTags: query.ListTags, + Password: password, + Username: username, + IdentityToken: idToken, + Filters: filters, } if _, found := r.URL.Query()["tlsVerify"]; found { options.SkipTLSVerify = types.NewOptionalBool(!query.TLSVerify) diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go index 6aefe50066..7172cf6c5f 100644 --- a/pkg/api/server/register_images.go +++ b/pkg/api/server/register_images.go @@ -1014,10 +1014,6 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // type: boolean // default: false // - in: query - // name: credentials - // description: "username:password for the registry" - // type: string - // - in: query // name: Arch // description: Pull image for the specified architecture. // type: string diff --git a/pkg/bindings/images/images.go b/pkg/bindings/images/images.go index ea7d445dba..ef76bb9dfc 100644 --- a/pkg/bindings/images/images.go +++ b/pkg/bindings/images/images.go @@ -288,7 +288,7 @@ func Search(ctx context.Context, term string, options *SearchOptions) ([]entitie params.Set("tlsVerify", strconv.FormatBool(!options.GetSkipTLSVerify())) } - header, err := auth.MakeXRegistryAuthHeader(&imageTypes.SystemContext{AuthFilePath: options.GetAuthfile()}, "", "") + header, err := auth.MakeXRegistryAuthHeader(&imageTypes.SystemContext{AuthFilePath: options.GetAuthfile()}, options.GetUsername(), options.GetPassword()) if err != nil { return nil, err } diff --git a/pkg/bindings/images/types.go b/pkg/bindings/images/types.go index 3f9e503ca9..a83ad206aa 100644 --- a/pkg/bindings/images/types.go +++ b/pkg/bindings/images/types.go @@ -145,7 +145,7 @@ type PushOptions struct { // Manifest type of the pushed image Format *string // Password for authenticating against the registry. - Password *string + Password *string `schema:"-"` // ProgressWriter is a writer where push progress are sent. // Since API handler for image push is quiet by default, WithQuiet(false) is necessary for // the writer to receive progress messages. @@ -155,7 +155,7 @@ type PushOptions struct { // RemoveSignatures Discard any pre-existing signatures in the image. RemoveSignatures *bool // Username for authenticating against the registry. - Username *string + Username *string `schema:"-"` // Quiet can be specified to suppress progress when pushing. Quiet *bool } @@ -175,6 +175,10 @@ type SearchOptions struct { SkipTLSVerify *bool `schema:"-"` // ListTags search the available tags of the repository ListTags *bool + // Username for authenticating against the registry. + Username *string `schema:"-"` + // Password for authenticating against the registry. + Password *string `schema:"-"` } // PullOptions are optional options for pulling images @@ -196,7 +200,7 @@ type PullOptions struct { // "newer", "always". An empty string defaults to "always". Policy *string // Password for authenticating against the registry. - Password *string + Password *string `schema:"-"` // ProgressWriter is a writer where pull progress are sent. ProgressWriter *io.Writer `schema:"-"` // Quiet can be specified to suppress pull progress when pulling. Ignored @@ -205,7 +209,7 @@ type PullOptions struct { // SkipTLSVerify to skip HTTPS and certificate verification. SkipTLSVerify *bool `schema:"-"` // Username for authenticating against the registry. - Username *string + Username *string `schema:"-"` // Variant will overwrite the local variant for image pulls. Variant *string } diff --git a/pkg/bindings/images/types_search_options.go b/pkg/bindings/images/types_search_options.go index 63de15c598..cc28da57f9 100644 --- a/pkg/bindings/images/types_search_options.go +++ b/pkg/bindings/images/types_search_options.go @@ -91,3 +91,33 @@ func (o *SearchOptions) GetListTags() bool { } return *o.ListTags } + +// WithUsername set field Username to given value +func (o *SearchOptions) WithUsername(value string) *SearchOptions { + o.Username = &value + return o +} + +// GetUsername returns value of field Username +func (o *SearchOptions) GetUsername() string { + if o.Username == nil { + var z string + return z + } + return *o.Username +} + +// WithPassword set field Password to given value +func (o *SearchOptions) WithPassword(value string) *SearchOptions { + o.Password = &value + return o +} + +// GetPassword returns value of field Password +func (o *SearchOptions) GetPassword() string { + if o.Password == nil { + var z string + return z + } + return *o.Password +} diff --git a/pkg/bindings/test/auth_test.go b/pkg/bindings/test/auth_test.go index 0846b707d9..aa6c940610 100644 --- a/pkg/bindings/test/auth_test.go +++ b/pkg/bindings/test/auth_test.go @@ -44,7 +44,7 @@ var _ = Describe("Podman images", func() { }) // Test using credentials. - It("tag + push + pull (with credentials)", func() { + It("tag + push + pull + search (with credentials)", func() { imageRep := "localhost:" + registry.Port + "/test" imageTag := "latest" @@ -65,6 +65,11 @@ var _ = Describe("Podman images", func() { pullOpts := new(images.PullOptions) _, err = images.Pull(bt.conn, imageRef, pullOpts.WithSkipTLSVerify(true).WithPassword(registry.Password).WithUsername(registry.User)) Expect(err).ToNot(HaveOccurred()) + + // Last, but not least, exercise search. + searchOptions := new(images.SearchOptions) + _, err = images.Search(bt.conn, imageRef, searchOptions.WithSkipTLSVerify(true).WithPassword(registry.Password).WithUsername(registry.User)) + Expect(err).ToNot(HaveOccurred()) }) // Test using authfile. diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go index 46c4a22e95..7232df5e39 100644 --- a/pkg/domain/entities/images.go +++ b/pkg/domain/entities/images.go @@ -261,6 +261,16 @@ type ImageSearchOptions struct { // Authfile is the path to the authentication file. Ignored for remote // calls. Authfile string + // CertDir is the path to certificate directories. Ignored for remote + // calls. + CertDir string + // Username for authenticating against the registry. + Username string + // Password for authenticating against the registry. + Password string + // IdentityToken is used to authenticate the user and get + // an access token for the registry. + IdentityToken string // Filters for the search results. Filters []string // Limit the number of results. diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go index 35ec312520..3aaf4f4018 100644 --- a/pkg/domain/infra/abi/images.go +++ b/pkg/domain/infra/abi/images.go @@ -461,6 +461,10 @@ func (ir *ImageEngine) Search(ctx context.Context, term string, opts entities.Im searchOptions := &libimage.SearchOptions{ Authfile: opts.Authfile, + CertDirPath: opts.CertDir, + Username: opts.Username, + Password: opts.Password, + IdentityToken: opts.IdentityToken, Filter: *filter, Limit: opts.Limit, NoTrunc: true, diff --git a/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go index 86e4313772..f249769f73 100644 --- a/pkg/domain/infra/tunnel/images.go +++ b/pkg/domain/infra/tunnel/images.go @@ -340,7 +340,7 @@ func (ir *ImageEngine) Search(ctx context.Context, term string, opts entities.Im options := new(images.SearchOptions) options.WithAuthfile(opts.Authfile).WithFilters(mappedFilters).WithLimit(opts.Limit) - options.WithListTags(opts.ListTags) + options.WithListTags(opts.ListTags).WithPassword(opts.Password).WithUsername(opts.Username) if s := opts.SkipTLSVerify; s != types.OptionalBoolUndefined { if s == types.OptionalBoolTrue { options.WithSkipTLSVerify(true) diff --git a/test/system/150-login.bats b/test/system/150-login.bats index 95d1c8bb2f..f909914c4f 100644 --- a/test/system/150-login.bats +++ b/test/system/150-login.bats @@ -70,9 +70,14 @@ function setup() { openssl req -newkey rsa:4096 -nodes -sha256 \ -keyout $AUTHDIR/domain.key -x509 -days 2 \ -out $AUTHDIR/domain.crt \ - -subj "/C=US/ST=Foo/L=Bar/O=Red Hat, Inc./CN=localhost" + -subj "/C=US/ST=Foo/L=Bar/O=Red Hat, Inc./CN=localhost" \ + -addext "subjectAltName=DNS:localhost" fi + # Copy a cert to another directory for --cert-dir option tests + mkdir -p ${PODMAN_LOGIN_WORKDIR}/trusted-registry-cert-dir + cp $CERT ${PODMAN_LOGIN_WORKDIR}/trusted-registry-cert-dir + # Store credentials where container will see them if [ ! -e $AUTHDIR/htpasswd ]; then htpasswd -Bbn ${PODMAN_LOGIN_USER} ${PODMAN_LOGIN_PASS} \ @@ -185,20 +190,42 @@ EOF "auth error on push" } -@test "podman push ok" { +function _push_search_test() { # Preserve image ID for later comparison against push/pulled image run_podman inspect --format '{{.Id}}' $IMAGE iid=$output destname=ok-$(random_string 10 | tr A-Z a-z)-ok # Use command-line credentials - run_podman push --tls-verify=false \ + run_podman push --tls-verify=$1 \ --format docker \ + --cert-dir ${PODMAN_LOGIN_WORKDIR}/trusted-registry-cert-dir \ --creds ${PODMAN_LOGIN_USER}:${PODMAN_LOGIN_PASS} \ $IMAGE localhost:${PODMAN_LOGIN_REGISTRY_PORT}/$destname + # Search a pushed image without --cert-dir will be failed if --tls-verify=true + run_podman $2 search --tls-verify=$1 \ + --format "table {{.Name}}" \ + --creds ${PODMAN_LOGIN_USER}:${PODMAN_LOGIN_PASS} \ + localhost:${PODMAN_LOGIN_REGISTRY_PORT}/$destname + + # Search a pushed image without --creds will be failed + run_podman 125 search --tls-verify=$1 \ + --format "table {{.Name}}" \ + --cert-dir ${PODMAN_LOGIN_WORKDIR}/trusted-registry-cert-dir \ + localhost:${PODMAN_LOGIN_REGISTRY_PORT}/$destname + + # Search a pushed image will be successed + run_podman search --tls-verify=$1 \ + --format "table {{.Name}}" \ + --cert-dir ${PODMAN_LOGIN_WORKDIR}/trusted-registry-cert-dir \ + --creds ${PODMAN_LOGIN_USER}:${PODMAN_LOGIN_PASS} \ + localhost:${PODMAN_LOGIN_REGISTRY_PORT}/$destname + is "${lines[1]}" "localhost:${PODMAN_LOGIN_REGISTRY_PORT}/$destname" "search output is destname" + # Yay! Pull it back - run_podman pull --tls-verify=false \ + run_podman pull --tls-verify=$1 \ + --cert-dir ${PODMAN_LOGIN_WORKDIR}/trusted-registry-cert-dir \ --creds ${PODMAN_LOGIN_USER}:${PODMAN_LOGIN_PASS} \ localhost:${PODMAN_LOGIN_REGISTRY_PORT}/$destname @@ -209,6 +236,14 @@ EOF run_podman rmi $destname } +@test "podman push and search ok with --tls-verify=false" { + _push_search_test false 0 +} + +@test "podman push and search ok with --tls-verify=true" { + _push_search_test true 125 +} + # END primary podman login/push/pull tests ############################################################################### # BEGIN cooperation with skopeo