diff --git a/pkg/api/handlers/compat/images.go b/pkg/api/handlers/compat/images.go index 04e59d58ff..0dc0d2bdad 100644 --- a/pkg/api/handlers/compat/images.go +++ b/pkg/api/handlers/compat/images.go @@ -15,7 +15,6 @@ import ( "github.com/containers/common/pkg/config" "github.com/containers/common/pkg/filters" "github.com/containers/image/v5/manifest" - "github.com/containers/image/v5/types" "github.com/containers/podman/v4/libpod" "github.com/containers/podman/v4/pkg/api/handlers" "github.com/containers/podman/v4/pkg/api/handlers/utils" @@ -25,10 +24,8 @@ import ( "github.com/containers/podman/v4/pkg/domain/infra/abi" "github.com/containers/podman/v4/pkg/util" "github.com/containers/storage" - "github.com/docker/distribution/registry/api/errcode" docker "github.com/docker/docker/api/types" dockerContainer "github.com/docker/docker/api/types/container" - "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/go-connections/nat" "github.com/opencontainers/go-digest" "github.com/sirupsen/logrus" @@ -253,11 +250,6 @@ func CreateImageFromSrc(w http.ResponseWriter, r *http.Request) { }) } -type pullResult struct { - images []*libimage.Image - err error -} - func CreateImageFromImage(w http.ResponseWriter, r *http.Request) { // 200 no error // 404 repo does not exist or no read access @@ -309,99 +301,7 @@ func CreateImageFromImage(w http.ResponseWriter, r *http.Request) { } } - progress := make(chan types.ProgressProperties) - pullOptions.Progress = progress - - pullResChan := make(chan pullResult) - go func() { - pulledImages, err := runtime.LibimageRuntime().Pull(r.Context(), possiblyNormalizedName, config.PullPolicyAlways, pullOptions) - pullResChan <- pullResult{images: pulledImages, err: err} - }() - - enc := json.NewEncoder(w) - enc.SetEscapeHTML(true) - - flush := func() { - if flusher, ok := w.(http.Flusher); ok { - flusher.Flush() - } - } - - statusWritten := false - writeStatusCode := func(code int) { - if !statusWritten { - w.WriteHeader(code) - w.Header().Set("Content-Type", "application/json") - flush() - statusWritten = true - } - } - -loop: // break out of for/select infinite loop - for { - report := jsonmessage.JSONMessage{} - report.Progress = &jsonmessage.JSONProgress{} - select { - case e := <-progress: - writeStatusCode(http.StatusOK) - switch e.Event { - case types.ProgressEventNewArtifact: - report.Status = "Pulling fs layer" - case types.ProgressEventRead: - report.Status = "Downloading" - report.Progress.Current = int64(e.Offset) - report.Progress.Total = e.Artifact.Size - report.ProgressMessage = report.Progress.String() - case types.ProgressEventSkipped: - report.Status = "Already exists" - case types.ProgressEventDone: - report.Status = "Download complete" - } - report.ID = e.Artifact.Digest.Encoded()[0:12] - if err := enc.Encode(report); err != nil { - logrus.Warnf("Failed to json encode error %q", err.Error()) - } - flush() - case pullRes := <-pullResChan: - err := pullRes.err - if err != nil { - var errcd errcode.ErrorCoder - if errors.As(err, &errcd) { - writeStatusCode(errcd.ErrorCode().Descriptor().HTTPStatusCode) - } else { - writeStatusCode(http.StatusInternalServerError) - } - msg := err.Error() - report.Error = &jsonmessage.JSONError{ - Message: msg, - } - report.ErrorMessage = msg - } else { - pulledImages := pullRes.images - if len(pulledImages) > 0 { - img := pulledImages[0].ID() - if utils.IsLibpodRequest(r) { - report.Status = "Pull complete" - } else { - report.Status = "Download complete" - } - report.ID = img[0:12] - } else { - msg := "internal error: no images pulled" - report.Error = &jsonmessage.JSONError{ - Message: msg, - } - report.ErrorMessage = msg - writeStatusCode(http.StatusInternalServerError) - } - } - if err := enc.Encode(report); err != nil { - logrus.Warnf("Failed to json encode error %q", err.Error()) - } - flush() - break loop // break out of for/select infinite loop - } - } + utils.CompatPull(r.Context(), w, runtime, possiblyNormalizedName, config.PullPolicyAlways, pullOptions) } func GetImage(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/api/handlers/libpod/images_pull.go b/pkg/api/handlers/libpod/images_pull.go index 57b2e3a787..0f50b44a45 100644 --- a/pkg/api/handlers/libpod/images_pull.go +++ b/pkg/api/handlers/libpod/images_pull.go @@ -28,14 +28,16 @@ func ImagesPull(w http.ResponseWriter, r *http.Request) { runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime) decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder) query := struct { - Reference string `schema:"reference"` - OS string `schema:"OS"` - Arch string `schema:"Arch"` - Variant string `schema:"Variant"` - TLSVerify bool `schema:"tlsVerify"` AllTags bool `schema:"allTags"` + CompatMode bool `schema:"compatMode"` PullPolicy string `schema:"policy"` Quiet bool `schema:"quiet"` + Reference string `schema:"reference"` + TLSVerify bool `schema:"tlsVerify"` + // Platform fields below: + Arch string `schema:"Arch"` + OS string `schema:"OS"` + Variant string `schema:"Variant"` }{ TLSVerify: true, PullPolicy: "always", @@ -46,6 +48,11 @@ func ImagesPull(w http.ResponseWriter, r *http.Request) { return } + if query.Quiet && query.CompatMode { + utils.InternalServerError(w, errors.New("'quiet' and 'compatMode' cannot be used simultaneously")) + return + } + if len(query.Reference) == 0 { utils.InternalServerError(w, errors.New("reference parameter cannot be empty")) return @@ -104,6 +111,11 @@ func ImagesPull(w http.ResponseWriter, r *http.Request) { return } + if query.CompatMode { + utils.CompatPull(r.Context(), w, runtime, query.Reference, pullPolicy, pullOptions) + return + } + writer := channel.NewWriter(make(chan []byte)) defer writer.Close() pullOptions.Writer = writer diff --git a/pkg/api/handlers/utils/images.go b/pkg/api/handlers/utils/images.go index 7831718cdb..58cda0e01f 100644 --- a/pkg/api/handlers/utils/images.go +++ b/pkg/api/handlers/utils/images.go @@ -1,12 +1,14 @@ package utils import ( + "context" "errors" "fmt" "net/http" "strings" "github.com/containers/common/libimage" + "github.com/containers/common/pkg/config" "github.com/containers/image/v5/docker" "github.com/containers/image/v5/docker/reference" storageTransport "github.com/containers/image/v5/storage" @@ -16,6 +18,9 @@ import ( api "github.com/containers/podman/v4/pkg/api/types" "github.com/containers/podman/v4/pkg/util" "github.com/containers/storage" + "github.com/docker/distribution/registry/api/errcode" + "github.com/docker/docker/pkg/jsonmessage" + "github.com/sirupsen/logrus" ) // NormalizeToDockerHub normalizes the specified nameOrID to Docker Hub if the @@ -102,3 +107,100 @@ func GetImage(r *http.Request, name string) (*libimage.Image, error) { } return image, err } + +type pullResult struct { + images []*libimage.Image + err error +} + +func CompatPull(ctx context.Context, w http.ResponseWriter, runtime *libpod.Runtime, reference string, pullPolicy config.PullPolicy, pullOptions *libimage.PullOptions) { + progress := make(chan types.ProgressProperties) + pullOptions.Progress = progress + + pullResChan := make(chan pullResult) + go func() { + pulledImages, err := runtime.LibimageRuntime().Pull(ctx, reference, pullPolicy, pullOptions) + pullResChan <- pullResult{images: pulledImages, err: err} + }() + + enc := json.NewEncoder(w) + enc.SetEscapeHTML(true) + + flush := func() { + if flusher, ok := w.(http.Flusher); ok { + flusher.Flush() + } + } + + statusWritten := false + writeStatusCode := func(code int) { + if !statusWritten { + w.WriteHeader(code) + w.Header().Set("Content-Type", "application/json") + flush() + statusWritten = true + } + } + +loop: // break out of for/select infinite loop + for { + report := jsonmessage.JSONMessage{} + report.Progress = &jsonmessage.JSONProgress{} + select { + case e := <-progress: + writeStatusCode(http.StatusOK) + switch e.Event { + case types.ProgressEventNewArtifact: + report.Status = "Pulling fs layer" + case types.ProgressEventRead: + report.Status = "Downloading" + report.Progress.Current = int64(e.Offset) + report.Progress.Total = e.Artifact.Size + report.ProgressMessage = report.Progress.String() + case types.ProgressEventSkipped: + report.Status = "Already exists" + case types.ProgressEventDone: + report.Status = "Download complete" + } + report.ID = e.Artifact.Digest.Encoded()[0:12] + if err := enc.Encode(report); err != nil { + logrus.Warnf("Failed to json encode error %q", err.Error()) + } + flush() + case pullRes := <-pullResChan: + err := pullRes.err + if err != nil { + var errcd errcode.ErrorCoder + if errors.As(err, &errcd) { + writeStatusCode(errcd.ErrorCode().Descriptor().HTTPStatusCode) + } else { + writeStatusCode(http.StatusInternalServerError) + } + msg := err.Error() + report.Error = &jsonmessage.JSONError{ + Message: msg, + } + report.ErrorMessage = msg + } else { + pulledImages := pullRes.images + if len(pulledImages) > 0 { + img := pulledImages[0].ID() + report.Status = "Download complete" + report.ID = img[0:12] + } else { + msg := "internal error: no images pulled" + report.Error = &jsonmessage.JSONError{ + Message: msg, + } + report.ErrorMessage = msg + writeStatusCode(http.StatusInternalServerError) + } + } + if err := enc.Encode(report); err != nil { + logrus.Warnf("Failed to json encode error %q", err.Error()) + } + flush() + break loop // break out of for/select infinite loop + } + } +} diff --git a/pkg/api/server/register_images.go b/pkg/api/server/register_images.go index 5645052497..4c14f54140 100644 --- a/pkg/api/server/register_images.go +++ b/pkg/api/server/register_images.go @@ -1019,6 +1019,11 @@ func (s *APIServer) registerImagesHandlers(r *mux.Router) error { // type: boolean // default: false // - in: query + // name: compatMode + // description: "Return the same JSON payload as the Docker-compat endpoint." + // type: boolean + // default: false + // - in: query // name: Arch // description: Pull image for the specified architecture. // type: string diff --git a/test/apiv2/10-images.at b/test/apiv2/10-images.at index be742b01b9..d4b4c8ac35 100644 --- a/test/apiv2/10-images.at +++ b/test/apiv2/10-images.at @@ -49,6 +49,7 @@ t GET images/$iid/json 200 \ .RepoTags[0]=$IMAGE t POST "images/create?fromImage=alpine" 200 .error~null .status~".*Download complete.*" +t POST "libpod/images/pull?reference=alpine&compatMode=true" 200 .error~null .status~".*Download complete.*" t POST "images/create?fromImage=alpine&tag=latest" 200