From 44a515015c3766809089d260917a508bf94a73fd Mon Sep 17 00:00:00 2001
From: Valentin Rothberg <rothberg@redhat.com>
Date: Tue, 31 Mar 2020 12:54:06 +0200
Subject: podmanV2: implement push

* Implement `podman-push` and `podman-image-push` for the podmanV2
  client.

* Tests for `pkg/bindings` are not possible at the time of writing as we
  don't have a local registry running.

* Implement `/images/{name}/push` compat endpoint. Tests are not
  implemented for this v2 endpoint.  It has been tested manually.

General note: The auth config extraction from the http header is not
implement for push.  Since it's not yet supported for other endpoints
either, I deferred it to future work.

Signed-off-by: Valentin Rothberg <rothberg@redhat.com>
---
 cmd/podmanV2/images/push.go            | 127 +++++++++++++++++++++++++++++++++
 pkg/api/handlers/compat/images_push.go |  80 +++++++++++++++++++++
 pkg/api/handlers/libpod/images.go      | 122 +++++++++++++++++++++++++------
 pkg/api/handlers/utils/images.go       |  41 +++++++++++
 pkg/api/server/register_images.go      |  72 +++++++++++++++++++
 pkg/bindings/images/images.go          |  24 +++++++
 pkg/domain/entities/engine_image.go    |   1 +
 pkg/domain/entities/images.go          |  37 ++++++++++
 pkg/domain/infra/abi/images.go         |  60 ++++++++++++++++
 pkg/domain/infra/tunnel/images.go      |   4 ++
 10 files changed, 547 insertions(+), 21 deletions(-)
 create mode 100644 cmd/podmanV2/images/push.go
 create mode 100644 pkg/api/handlers/compat/images_push.go

diff --git a/cmd/podmanV2/images/push.go b/cmd/podmanV2/images/push.go
new file mode 100644
index 000000000..82cc0c486
--- /dev/null
+++ b/cmd/podmanV2/images/push.go
@@ -0,0 +1,127 @@
+package images
+
+import (
+	buildahcli "github.com/containers/buildah/pkg/cli"
+	"github.com/containers/image/v5/types"
+	"github.com/containers/libpod/cmd/podmanV2/registry"
+	"github.com/containers/libpod/pkg/domain/entities"
+	"github.com/pkg/errors"
+	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
+)
+
+// pushOptionsWrapper wraps entities.ImagepushOptions and prevents leaking
+// CLI-only fields into the API types.
+type pushOptionsWrapper struct {
+	entities.ImagePushOptions
+	TLSVerifyCLI bool // CLI only
+}
+
+var (
+	pushOptions     = pushOptionsWrapper{}
+	pushDescription = `Pushes a source image to a specified destination.
+
+	The Image "DESTINATION" uses a "transport":"details" format. See podman-push(1) section "DESTINATION" for the expected format.`
+
+	// Command: podman push
+	pushCmd = &cobra.Command{
+		Use:     "push [flags] SOURCE DESTINATION",
+		Short:   "Push an image to a specified destination",
+		Long:    pushDescription,
+		PreRunE: preRunE,
+		RunE:    imagePush,
+		Example: `podman push imageID docker://registry.example.com/repository:tag
+		podman push imageID oci-archive:/path/to/layout:image:tag`,
+	}
+
+	// Command: podman image push
+	// It's basically a clone of `pushCmd` with the exception of being a
+	// child of the images command.
+	imagePushCmd = &cobra.Command{
+		Use:     pushCmd.Use,
+		Short:   pushCmd.Short,
+		Long:    pushCmd.Long,
+		PreRunE: pushCmd.PreRunE,
+		RunE:    pushCmd.RunE,
+		Example: `podman image push imageID docker://registry.example.com/repository:tag
+		podman image push imageID oci-archive:/path/to/layout:image:tag`,
+	}
+)
+
+func init() {
+	// push
+	registry.Commands = append(registry.Commands, registry.CliCommand{
+		Mode:    []entities.EngineMode{entities.ABIMode, entities.TunnelMode},
+		Command: pushCmd,
+	})
+
+	pushCmd.SetHelpTemplate(registry.HelpTemplate())
+	pushCmd.SetUsageTemplate(registry.UsageTemplate())
+
+	flags := pushCmd.Flags()
+	pushFlags(flags)
+
+	// images push
+	registry.Commands = append(registry.Commands, registry.CliCommand{
+		Mode:    []entities.EngineMode{entities.ABIMode, entities.TunnelMode},
+		Command: imagePushCmd,
+		Parent:  imageCmd,
+	})
+
+	imagePushCmd.SetHelpTemplate(registry.HelpTemplate())
+	imagePushCmd.SetUsageTemplate(registry.UsageTemplate())
+	pushFlags(imagePushCmd.Flags())
+}
+
+// pushFlags set the flags for the push command.
+func pushFlags(flags *pflag.FlagSet) {
+	flags.StringVar(&pushOptions.Authfile, "authfile", buildahcli.GetDefaultAuthFile(), "Path of the authentication file. Use REGISTRY_AUTH_FILE environment variable to override")
+	flags.StringVar(&pushOptions.CertDir, "cert-dir", "", "Path to a directory containing TLS certificates and keys")
+	flags.BoolVar(&pushOptions.Compress, "compress", false, "Compress tarball image layers when pushing to a directory using the 'dir' transport. (default is same compression type as source)")
+	flags.StringVar(&pushOptions.Credentials, "creds", "", "`Credentials` (USERNAME:PASSWORD) to use for authenticating to a registry")
+	flags.StringVar(&pushOptions.DigestFile, "digestfile", "", "Write the digest of the pushed image to the specified file")
+	flags.StringVarP(&pushOptions.Format, "format", "f", "", "Manifest type (oci, v2s1, or v2s2) to use when pushing an image using the 'dir' transport (default is manifest type of source)")
+	flags.BoolVarP(&pushOptions.Quiet, "quiet", "q", false, "Suppress output information when pushing images")
+	flags.BoolVar(&pushOptions.RemoveSignatures, "remove-signatures", false, "Discard any pre-existing signatures in the image")
+	flags.StringVar(&pushOptions.SignaturePolicy, "signature-policy", "", "Path to a signature-policy file")
+	flags.StringVar(&pushOptions.SignBy, "sign-by", "", "Add a signature at the destination using the specified key")
+	flags.BoolVar(&pushOptions.TLSVerifyCLI, "tls-verify", true, "Require HTTPS and verify certificates when contacting registries")
+
+	if registry.IsRemote() {
+		_ = flags.MarkHidden("authfile")
+		_ = flags.MarkHidden("cert-dir")
+		_ = flags.MarkHidden("compress")
+		_ = flags.MarkHidden("quiet")
+		_ = flags.MarkHidden("signature-policy")
+		_ = flags.MarkHidden("tls-verify")
+	}
+}
+
+// imagePush is implement the command for pushing images.
+func imagePush(cmd *cobra.Command, args []string) error {
+	var source, destination string
+	switch len(args) {
+	case 1:
+		source = args[0]
+	case 2:
+		source = args[0]
+		destination = args[1]
+	case 0:
+		fallthrough
+	default:
+		return errors.New("push requires at least one image name, or optionally a second to specify a different destination")
+	}
+
+	pushOptsAPI := pushOptions.ImagePushOptions
+	// 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") {
+		pushOptsAPI.TLSVerify = types.NewOptionalBool(pushOptions.TLSVerifyCLI)
+	}
+
+	// Let's do all the remaining Yoga in the API to prevent us from scattering
+	// logic across (too) many parts of the code.
+	return registry.ImageEngine().Push(registry.GetContext(), source, destination, pushOptsAPI)
+}
diff --git a/pkg/api/handlers/compat/images_push.go b/pkg/api/handlers/compat/images_push.go
new file mode 100644
index 000000000..2260d5557
--- /dev/null
+++ b/pkg/api/handlers/compat/images_push.go
@@ -0,0 +1,80 @@
+package compat
+
+import (
+	"context"
+	"net/http"
+	"os"
+	"strings"
+
+	"github.com/containers/libpod/libpod"
+	"github.com/containers/libpod/libpod/image"
+	"github.com/containers/libpod/pkg/api/handlers/utils"
+	"github.com/gorilla/schema"
+	"github.com/pkg/errors"
+)
+
+// PushImage is the handler for the compat http endpoint for pushing images.
+func PushImage(w http.ResponseWriter, r *http.Request) {
+	decoder := r.Context().Value("decoder").(*schema.Decoder)
+	runtime := r.Context().Value("runtime").(*libpod.Runtime)
+
+	query := struct {
+		Tag string `schema:"tag"`
+	}{
+		// 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
+	}
+
+	// Note that Docker's docs state "Image name or ID" to be in the path
+	// parameter but it really must be a name as Docker does not allow for
+	// pushing an image by ID.
+	imageName := strings.TrimSuffix(utils.GetName(r), "/push") // GetName returns the entire path
+	if query.Tag != "" {
+		imageName += ":" + query.Tag
+	}
+	if _, err := utils.ParseStorageReference(imageName); err != nil {
+		utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest,
+			errors.Wrapf(err, "image source %q is not a containers-storage-transport reference", imageName))
+		return
+	}
+
+	newImage, err := runtime.ImageRuntime().NewFromLocal(imageName)
+	if err != nil {
+		utils.ImageNotFound(w, imageName, errors.Wrapf(err, "Failed to find image %s", imageName))
+		return
+	}
+
+	// TODO: the X-Registry-Auth header is not checked yet here nor in any other
+	// endpoint. Pushing does NOT work with authentication at the moment.
+	dockerRegistryOptions := &image.DockerRegistryOptions{}
+	authfile := ""
+	if sys := runtime.SystemContext(); sys != nil {
+		dockerRegistryOptions.DockerCertPath = sys.DockerCertPath
+		authfile = sys.AuthFilePath
+	}
+
+	err = newImage.PushImageToHeuristicDestination(
+		context.Background(),
+		imageName,
+		"", // manifest type
+		authfile,
+		"", // digest file
+		"", // signature policy
+		os.Stderr,
+		false, // force compression
+		image.SigningOptions{},
+		dockerRegistryOptions,
+		nil, // additional tags
+	)
+	if err != nil {
+		utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Error pushing image %q", imageName))
+		return
+	}
+
+	utils.WriteResponse(w, http.StatusOK, "")
+
+}
diff --git a/pkg/api/handlers/libpod/images.go b/pkg/api/handlers/libpod/images.go
index d4fd77cd7..e7f20854c 100644
--- a/pkg/api/handlers/libpod/images.go
+++ b/pkg/api/handlers/libpod/images.go
@@ -14,7 +14,6 @@ import (
 	"github.com/containers/image/v5/docker"
 	"github.com/containers/image/v5/docker/reference"
 	"github.com/containers/image/v5/manifest"
-	"github.com/containers/image/v5/transports/alltransports"
 	"github.com/containers/image/v5/types"
 	"github.com/containers/libpod/libpod"
 	"github.com/containers/libpod/libpod/image"
@@ -331,29 +330,16 @@ func ImagesPull(w http.ResponseWriter, r *http.Request) {
 		utils.InternalServerError(w, errors.New("reference parameter cannot be empty"))
 		return
 	}
-	// Enforce the docker transport.  This is just a precaution as some callers
-	// might be accustomed to using the "transport:reference" notation.  Using
-	// another than the "docker://" transport does not really make sense for a
-	// remote case. For loading tarballs, the load and import endpoints should
-	// be used.
-	dockerPrefix := fmt.Sprintf("%s://", docker.Transport.Name())
-	imageRef, err := alltransports.ParseImageName(query.Reference)
-	if err == nil && imageRef.Transport().Name() != docker.Transport.Name() {
+
+	imageRef, err := utils.ParseDockerReference(query.Reference)
+	if err != nil {
 		utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest,
-			errors.Errorf("reference %q must be a docker reference", query.Reference))
+			errors.Wrapf(err, "image destination %q is not a docker-transport reference", query.Reference))
 		return
-	} else if err != nil {
-		origErr := err
-		imageRef, err = alltransports.ParseImageName(fmt.Sprintf("%s%s", dockerPrefix, query.Reference))
-		if err != nil {
-			utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest,
-				errors.Wrapf(origErr, "reference %q must be a docker reference", query.Reference))
-			return
-		}
 	}
 
 	// Trim the docker-transport prefix.
-	rawImage := strings.TrimPrefix(query.Reference, dockerPrefix)
+	rawImage := strings.TrimPrefix(query.Reference, fmt.Sprintf("%s://", docker.Transport.Name()))
 
 	// all-tags doesn't work with a tagged reference, so let's check early
 	namedRef, err := reference.Parse(rawImage)
@@ -385,7 +371,7 @@ func ImagesPull(w http.ResponseWriter, r *http.Request) {
 		OSChoice:            query.OverrideOS,
 		ArchitectureChoice:  query.OverrideArch,
 	}
-	if query.TLSVerify {
+	if _, found := r.URL.Query()["tlsVerify"]; found {
 		dockerRegistryOptions.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!query.TLSVerify)
 	}
 
@@ -408,13 +394,19 @@ func ImagesPull(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
+	authfile := ""
+	if sys := runtime.SystemContext(); sys != nil {
+		dockerRegistryOptions.DockerCertPath = sys.DockerCertPath
+		authfile = sys.AuthFilePath
+	}
+
 	// Finally pull the images
 	for _, img := range imagesToPull {
 		newImage, err := runtime.ImageRuntime().New(
 			context.Background(),
 			img,
 			"",
-			"",
+			authfile,
 			os.Stderr,
 			&dockerRegistryOptions,
 			image.SigningOptions{},
@@ -430,6 +422,94 @@ func ImagesPull(w http.ResponseWriter, r *http.Request) {
 	utils.WriteResponse(w, http.StatusOK, res)
 }
 
+// PushImage is the handler for the compat http endpoint for pushing images.
+func PushImage(w http.ResponseWriter, r *http.Request) {
+	decoder := r.Context().Value("decoder").(*schema.Decoder)
+	runtime := r.Context().Value("runtime").(*libpod.Runtime)
+
+	query := struct {
+		Credentials string `schema:"credentials"`
+		Destination string `schema:"destination"`
+		TLSVerify   bool   `schema:"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
+	}
+
+	source := strings.TrimSuffix(utils.GetName(r), "/push") // GetName returns the entire path
+	if _, err := utils.ParseStorageReference(source); err != nil {
+		utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest,
+			errors.Wrapf(err, "image source %q is not a containers-storage-transport reference", source))
+		return
+	}
+
+	destination := query.Destination
+	if destination == "" {
+		destination = source
+	}
+
+	if _, err := utils.ParseDockerReference(destination); err != nil {
+		utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest,
+			errors.Wrapf(err, "image destination %q is not a docker-transport reference", destination))
+		return
+	}
+
+	newImage, err := runtime.ImageRuntime().NewFromLocal(source)
+	if err != nil {
+		utils.ImageNotFound(w, source, errors.Wrapf(err, "Failed to find image %s", source))
+		return
+	}
+
+	var registryCreds *types.DockerAuthConfig
+	if len(query.Credentials) != 0 {
+		creds, err := util.ParseRegistryCreds(query.Credentials)
+		if err != nil {
+			utils.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest,
+				errors.Wrapf(err, "error parsing credentials %q", query.Credentials))
+			return
+		}
+		registryCreds = creds
+	}
+
+	// TODO: the X-Registry-Auth header is not checked yet here nor in any other
+	// endpoint. Pushing does NOT work with authentication at the moment.
+	dockerRegistryOptions := &image.DockerRegistryOptions{
+		DockerRegistryCreds: registryCreds,
+	}
+	authfile := ""
+	if sys := runtime.SystemContext(); sys != nil {
+		dockerRegistryOptions.DockerCertPath = sys.DockerCertPath
+		authfile = sys.AuthFilePath
+	}
+	if _, found := r.URL.Query()["tlsVerify"]; found {
+		dockerRegistryOptions.DockerInsecureSkipTLSVerify = types.NewOptionalBool(!query.TLSVerify)
+	}
+
+	err = newImage.PushImageToHeuristicDestination(
+		context.Background(),
+		destination,
+		"", // manifest type
+		authfile,
+		"", // digest file
+		"", // signature policy
+		os.Stderr,
+		false, // force compression
+		image.SigningOptions{},
+		dockerRegistryOptions,
+		nil, // additional tags
+	)
+	if err != nil {
+		utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "Error pushing image %q", destination))
+		return
+	}
+
+	utils.WriteResponse(w, http.StatusOK, "")
+}
+
 func CommitContainer(w http.ResponseWriter, r *http.Request) {
 	var (
 		destImage string
diff --git a/pkg/api/handlers/utils/images.go b/pkg/api/handlers/utils/images.go
index 696d5f745..1c67de9db 100644
--- a/pkg/api/handlers/utils/images.go
+++ b/pkg/api/handlers/utils/images.go
@@ -4,11 +4,52 @@ import (
 	"fmt"
 	"net/http"
 
+	"github.com/containers/image/v5/docker"
+	"github.com/containers/image/v5/storage"
+	"github.com/containers/image/v5/transports/alltransports"
+	"github.com/containers/image/v5/types"
 	"github.com/containers/libpod/libpod"
 	"github.com/containers/libpod/libpod/image"
 	"github.com/gorilla/schema"
+	"github.com/pkg/errors"
 )
 
+// ParseDockerReference parses the specified image name to a
+// `types.ImageReference` and enforces it to refer to a docker-transport
+// reference.
+func ParseDockerReference(name string) (types.ImageReference, error) {
+	dockerPrefix := fmt.Sprintf("%s://", docker.Transport.Name())
+	imageRef, err := alltransports.ParseImageName(name)
+	if err == nil && imageRef.Transport().Name() != docker.Transport.Name() {
+		return nil, errors.Errorf("reference %q must be a docker reference", name)
+	} else if err != nil {
+		origErr := err
+		imageRef, err = alltransports.ParseImageName(fmt.Sprintf("%s%s", dockerPrefix, name))
+		if err != nil {
+			return nil, errors.Wrapf(origErr, "reference %q must be a docker reference", name)
+		}
+	}
+	return imageRef, nil
+}
+
+// ParseStorageReference parses the specified image name to a
+// `types.ImageReference` and enforces it to refer to a
+// containers-storage-transport reference.
+func ParseStorageReference(name string) (types.ImageReference, error) {
+	storagePrefix := fmt.Sprintf("%s:", storage.Transport.Name())
+	imageRef, err := alltransports.ParseImageName(name)
+	if err == nil && imageRef.Transport().Name() != docker.Transport.Name() {
+		return nil, errors.Errorf("reference %q must be a storage reference", name)
+	} else if err != nil {
+		origErr := err
+		imageRef, err = alltransports.ParseImageName(fmt.Sprintf("%s%s", storagePrefix, name))
+		if err != nil {
+			return nil, errors.Wrapf(origErr, "reference %q must be a storage reference", name)
+		}
+	}
+	return imageRef, nil
+}
+
 // GetImages is a common function used to get images for libpod and other compatibility
 // mechanisms
 func GetImages(w http.ResponseWriter, r *http.Request) ([]*image.Image, error) {
diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go
index 74b245a77..e4e46025b 100644
--- a/pkg/api/server/register_images.go
+++ b/pkg/api/server/register_images.go
@@ -211,6 +211,41 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error {
 	r.Handle(VersionedPath("/images/{name:.*}"), s.APIHandler(compat.RemoveImage)).Methods(http.MethodDelete)
 	// Added non version path to URI to support docker non versioned paths
 	r.Handle("/images/{name:.*}", s.APIHandler(compat.RemoveImage)).Methods(http.MethodDelete)
+	// swagger:operation POST /images/{name:.*}/push compat pushImage
+	// ---
+	// tags:
+	//  - images (compat)
+	// summary: Push Image
+	// description: Push an image to a container registry
+	// parameters:
+	//  - in: path
+	//    name: name:.*
+	//    type: string
+	//    required: true
+	//    description: Name of image to push.
+	//  - in: query
+	//    name: tag
+	//    type: string
+	//    description: The tag to associate with the image on the registry.
+	//  - in: header
+	//    name: X-Registry-Auth
+	//    type: string
+	//    description: A base64-encoded auth configuration.
+	// produces:
+	// - application/json
+	// responses:
+	//   200:
+	//     description: no error
+	//     schema:
+	//      type: string
+	//      format: binary
+	//   404:
+	//     $ref: '#/responses/NoSuchImage'
+	//   500:
+	//     $ref: '#/responses/InternalError'
+	r.Handle(VersionedPath("/images/{name:.*}/push"), s.APIHandler(compat.PushImage)).Methods(http.MethodPost)
+	// Added non version path to URI to support docker non versioned paths
+	r.Handle("/images/{name:.*}/push", s.APIHandler(compat.PushImage)).Methods(http.MethodPost)
 	// swagger:operation GET /images/{name:.*}/get compat exportImage
 	// ---
 	// tags:
@@ -583,6 +618,43 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error {
 		libpod endpoints
 	*/
 
+	// swagger:operation POST /libpod/images/{name:.*}/push libpod libpodPushImage
+	// ---
+	// tags:
+	//  - images (libpod)
+	// summary: Push Image
+	// description: Push an image to a container registry
+	// parameters:
+	//  - in: path
+	//    name: name:.*
+	//    type: string
+	//    required: true
+	//    description: Name of image to push.
+	//  - in: query
+	//    name: tag
+	//    type: string
+	//    description: The tag to associate with the image on the registry.
+	//  - in: query
+	//    name: credentials
+	//    description: username:password for the registry.
+	//    type: string
+	//  - in: header
+	//    name: X-Registry-Auth
+	//    type: string
+	//    description: A base64-encoded auth configuration.
+	// produces:
+	// - application/json
+	// responses:
+	//   200:
+	//     description: no error
+	//     schema:
+	//      type: string
+	//      format: binary
+	//   404:
+	//     $ref: '#/responses/NoSuchImage'
+	//   500:
+	//     $ref: '#/responses/InternalError'
+	r.Handle(VersionedPath("/libpod/images/{name:.*}/push"), s.APIHandler(libpod.PushImage)).Methods(http.MethodPost)
 	// swagger:operation GET /libpod/images/{name:.*}/exists libpod libpodImageExists
 	// ---
 	// tags:
diff --git a/pkg/bindings/images/images.go b/pkg/bindings/images/images.go
index 470ce546c..dcb568d6b 100644
--- a/pkg/bindings/images/images.go
+++ b/pkg/bindings/images/images.go
@@ -3,6 +3,7 @@ package images
 import (
 	"context"
 	"errors"
+	"fmt"
 	"io"
 	"net/http"
 	"net/url"
@@ -283,3 +284,26 @@ func Pull(ctx context.Context, rawImage string, options entities.ImagePullOption
 
 	return pulledImages, nil
 }
+
+// Push is the binding for libpod's v2 endpoints for push images.  Note that
+// `source` must be a refering to an image in the remote's container storage.
+// The destination must be a reference to a registry (i.e., of docker transport
+// or be normalized to one).  Other transports are rejected as they do not make
+// sense in a remote context.
+func Push(ctx context.Context, source string, destination string, options entities.ImagePushOptions) error {
+	conn, err := bindings.GetClient(ctx)
+	if err != nil {
+		return err
+	}
+	params := url.Values{}
+	params.Set("credentials", options.Credentials)
+	params.Set("destination", destination)
+	if options.TLSVerify != types.OptionalBoolUndefined {
+		val := bool(options.TLSVerify == types.OptionalBoolTrue)
+		params.Set("tlsVerify", strconv.FormatBool(val))
+	}
+
+	path := fmt.Sprintf("/images/%s/push", source)
+	_, err = conn.DoRequest(nil, http.MethodPost, path, params)
+	return err
+}
diff --git a/pkg/domain/entities/engine_image.go b/pkg/domain/entities/engine_image.go
index ac856c05f..04b9d34e6 100644
--- a/pkg/domain/entities/engine_image.go
+++ b/pkg/domain/entities/engine_image.go
@@ -16,4 +16,5 @@ type ImageEngine interface {
 	Untag(ctx context.Context, nameOrId string, tags []string, options ImageUntagOptions) error
 	Load(ctx context.Context, opts ImageLoadOptions) (*ImageLoadReport, error)
 	Import(ctx context.Context, opts ImageImportOptions) (*ImageImportReport, error)
+	Push(ctx context.Context, source string, destination string, opts ImagePushOptions) error
 }
diff --git a/pkg/domain/entities/images.go b/pkg/domain/entities/images.go
index d136f42fd..d66de3c5e 100644
--- a/pkg/domain/entities/images.go
+++ b/pkg/domain/entities/images.go
@@ -144,6 +144,43 @@ type ImagePullReport struct {
 	Images []string
 }
 
+// ImagePushOptions are the arguments for pushing images.
+type ImagePushOptions 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
+	// Compress tarball image layers when pushing to a directory using the 'dir'
+	// transport. Default is same compression type as source. Ignored for remote
+	// calls.
+	Compress bool
+	// Credentials for authenticating against the registry in the format
+	// USERNAME:PASSWORD.
+	Credentials string
+	// DigestFile, after copying the image, write the digest of the resulting
+	// image to the file.  Ignored for remote calls.
+	DigestFile string
+	// Format is the Manifest type (oci, v2s1, or v2s2) to use when pushing an
+	// image using the 'dir' transport. Default is manifest type of source.
+	// Ignored for remote calls.
+	Format string
+	// Quiet can be specified to suppress pull progress when pulling.  Ignored
+	// for remote calls.
+	Quiet bool
+	// RemoveSignatures, discard any pre-existing signatures in the image.
+	// Ignored for remote calls.
+	RemoveSignatures bool
+	// SignaturePolicy to use when pulling.  Ignored for remote calls.
+	SignaturePolicy string
+	// SignBy adds a signature at the destination using the specified key.
+	// Ignored for remote calls.
+	SignBy string
+	// TLSVerify to enable/disable HTTPS and certificate verification.
+	TLSVerify types.OptionalBool
+}
+
 type ImageListOptions struct {
 	All     bool       `json:"all" schema:"all"`
 	Filter  []string   `json:"Filter,omitempty"`
diff --git a/pkg/domain/infra/abi/images.go b/pkg/domain/infra/abi/images.go
index f8c63730c..94008f287 100644
--- a/pkg/domain/infra/abi/images.go
+++ b/pkg/domain/infra/abi/images.go
@@ -12,6 +12,7 @@ import (
 	"github.com/containers/image/v5/docker"
 	dockerarchive "github.com/containers/image/v5/docker/archive"
 	"github.com/containers/image/v5/docker/reference"
+	"github.com/containers/image/v5/manifest"
 	"github.com/containers/image/v5/transports/alltransports"
 	"github.com/containers/image/v5/types"
 	"github.com/containers/libpod/libpod/image"
@@ -20,6 +21,7 @@ import (
 	domainUtils "github.com/containers/libpod/pkg/domain/utils"
 	"github.com/containers/libpod/pkg/util"
 	"github.com/containers/storage"
+	imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
 )
@@ -260,6 +262,64 @@ func (ir *ImageEngine) Inspect(ctx context.Context, names []string, opts entitie
 	return &report, nil
 }
 
+func (ir *ImageEngine) Push(ctx context.Context, source string, destination string, options entities.ImagePushOptions) error {
+	var writer io.Writer
+	if !options.Quiet {
+		writer = os.Stderr
+	}
+
+	var manifestType string
+	switch options.Format {
+	case "":
+		// Default
+	case "oci":
+		manifestType = imgspecv1.MediaTypeImageManifest
+	case "v2s1":
+		manifestType = manifest.DockerV2Schema1SignedMediaType
+	case "v2s2", "docker":
+		manifestType = manifest.DockerV2Schema2MediaType
+	default:
+		return fmt.Errorf("unknown format %q. Choose on of the supported formats: 'oci', 'v2s1', or 'v2s2'", options.Format)
+	}
+
+	var registryCreds *types.DockerAuthConfig
+	if options.Credentials != "" {
+		creds, err := util.ParseRegistryCreds(options.Credentials)
+		if err != nil {
+			return err
+		}
+		registryCreds = creds
+	}
+	dockerRegistryOptions := image.DockerRegistryOptions{
+		DockerRegistryCreds:         registryCreds,
+		DockerCertPath:              options.CertDir,
+		DockerInsecureSkipTLSVerify: options.TLSVerify,
+	}
+
+	signOptions := image.SigningOptions{
+		RemoveSignatures: options.RemoveSignatures,
+		SignBy:           options.SignBy,
+	}
+
+	newImage, err := ir.Libpod.ImageRuntime().NewFromLocal(source)
+	if err != nil {
+		return err
+	}
+
+	return newImage.PushImageToHeuristicDestination(
+		ctx,
+		destination,
+		manifestType,
+		options.Authfile,
+		options.DigestFile,
+		options.SignaturePolicy,
+		writer,
+		options.Compress,
+		signOptions,
+		&dockerRegistryOptions,
+		nil)
+}
+
 // func (r *imageRuntime) Delete(ctx context.Context, nameOrId string, opts entities.ImageDeleteOptions) (*entities.ImageDeleteReport, error) {
 // 	image, err := r.libpod.ImageEngine().NewFromLocal(nameOrId)
 // 	if err != nil {
diff --git a/pkg/domain/infra/tunnel/images.go b/pkg/domain/infra/tunnel/images.go
index 155f5e4bd..028603d98 100644
--- a/pkg/domain/infra/tunnel/images.go
+++ b/pkg/domain/infra/tunnel/images.go
@@ -184,3 +184,7 @@ func (ir *ImageEngine) Import(ctx context.Context, opts entities.ImageImportOpti
 	}
 	return images.Import(ir.ClientCxt, opts.Changes, &opts.Message, &opts.Reference, sourceURL, f)
 }
+
+func (ir *ImageEngine) Push(ctx context.Context, source string, destination string, options entities.ImagePushOptions) error {
+	return images.Push(ir.ClientCxt, source, destination, options)
+}
-- 
cgit v1.2.3-54-g00ecf