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 | |
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')
-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 | ||||
-rw-r--r-- | pkg/bindings/images/images.go | 24 | ||||
-rw-r--r-- | pkg/domain/entities/engine_image.go | 1 | ||||
-rw-r--r-- | pkg/domain/entities/images.go | 37 | ||||
-rw-r--r-- | pkg/domain/infra/abi/images.go | 60 | ||||
-rw-r--r-- | pkg/domain/infra/tunnel/images.go | 4 |
9 files changed, 420 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: 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) +} |