diff options
author | Valentin Rothberg <rothberg@redhat.com> | 2020-03-31 12:54:06 +0200 |
---|---|---|
committer | Valentin Rothberg <rothberg@redhat.com> | 2020-04-02 17:01:32 +0200 |
commit | 44a515015c3766809089d260917a508bf94a73fd (patch) | |
tree | 87847c6b13be0d7e3fd2c52496876288e9a41f1e /pkg/api | |
parent | ffd2d783919e6038fe55e3e6b8cf44c0b3356a96 (diff) | |
download | podman-44a515015c3766809089d260917a508bf94a73fd.tar.gz podman-44a515015c3766809089d260917a508bf94a73fd.tar.bz2 podman-44a515015c3766809089d260917a508bf94a73fd.zip |
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>
Diffstat (limited to 'pkg/api')
-rw-r--r-- | pkg/api/handlers/compat/images_push.go | 80 | ||||
-rw-r--r-- | pkg/api/handlers/libpod/images.go | 122 | ||||
-rw-r--r-- | pkg/api/handlers/utils/images.go | 41 | ||||
-rw-r--r-- | pkg/api/server/register_images.go | 72 |
4 files changed, 294 insertions, 21 deletions
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: |