Skip to content

Commit b7c00f2

Browse files
Merge pull request #9647 from mlegenovic/master
Compat API: Fix the response of 'push image' endpoint
2 parents 4174b06 + 9fc29f6 commit b7c00f2

File tree

6 files changed

+117
-26
lines changed

6 files changed

+117
-26
lines changed

libpod/image/image.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -816,7 +816,7 @@ func (i *Image) UntagImage(tag string) error {
816816

817817
// PushImageToHeuristicDestination pushes the given image to "destination", which is heuristically parsed.
818818
// Use PushImageToReference if the destination is known precisely.
819-
func (i *Image) PushImageToHeuristicDestination(ctx context.Context, destination, manifestMIMEType, authFile, digestFile, signaturePolicyPath string, writer io.Writer, forceCompress bool, signingOptions SigningOptions, dockerRegistryOptions *DockerRegistryOptions, additionalDockerArchiveTags []reference.NamedTagged) error {
819+
func (i *Image) PushImageToHeuristicDestination(ctx context.Context, destination, manifestMIMEType, authFile, digestFile, signaturePolicyPath string, writer io.Writer, forceCompress bool, signingOptions SigningOptions, dockerRegistryOptions *DockerRegistryOptions, additionalDockerArchiveTags []reference.NamedTagged, progress chan types.ProgressProperties) error {
820820
if destination == "" {
821821
return errors.Wrapf(syscall.EINVAL, "destination image name must be specified")
822822
}
@@ -834,11 +834,11 @@ func (i *Image) PushImageToHeuristicDestination(ctx context.Context, destination
834834
return err
835835
}
836836
}
837-
return i.PushImageToReference(ctx, dest, manifestMIMEType, authFile, digestFile, signaturePolicyPath, writer, forceCompress, signingOptions, dockerRegistryOptions, additionalDockerArchiveTags)
837+
return i.PushImageToReference(ctx, dest, manifestMIMEType, authFile, digestFile, signaturePolicyPath, writer, forceCompress, signingOptions, dockerRegistryOptions, additionalDockerArchiveTags, progress)
838838
}
839839

840840
// PushImageToReference pushes the given image to a location described by the given path
841-
func (i *Image) PushImageToReference(ctx context.Context, dest types.ImageReference, manifestMIMEType, authFile, digestFile, signaturePolicyPath string, writer io.Writer, forceCompress bool, signingOptions SigningOptions, dockerRegistryOptions *DockerRegistryOptions, additionalDockerArchiveTags []reference.NamedTagged) error {
841+
func (i *Image) PushImageToReference(ctx context.Context, dest types.ImageReference, manifestMIMEType, authFile, digestFile, signaturePolicyPath string, writer io.Writer, forceCompress bool, signingOptions SigningOptions, dockerRegistryOptions *DockerRegistryOptions, additionalDockerArchiveTags []reference.NamedTagged, progress chan types.ProgressProperties) error {
842842
sc := GetSystemContext(signaturePolicyPath, authFile, forceCompress)
843843
sc.BlobInfoCacheDir = filepath.Join(i.imageruntime.store.GraphRoot(), "cache")
844844

@@ -859,6 +859,10 @@ func (i *Image) PushImageToReference(ctx context.Context, dest types.ImageRefere
859859
}
860860
copyOptions := getCopyOptions(sc, writer, nil, dockerRegistryOptions, signingOptions, manifestMIMEType, additionalDockerArchiveTags)
861861
copyOptions.DestinationCtx.SystemRegistriesConfPath = registries.SystemRegistriesConfPath() // FIXME: Set this more globally. Probably no reason not to have it in every types.SystemContext, and to compute the value just once in one place.
862+
if progress != nil {
863+
copyOptions.Progress = progress
864+
copyOptions.ProgressInterval = time.Second
865+
}
862866
// Copy the image to the remote destination
863867
manifestBytes, err := cp.Image(ctx, policyContext, dest, src, copyOptions)
864868
if err != nil {
@@ -1648,7 +1652,7 @@ func (i *Image) Save(ctx context.Context, source, format, output string, moreTag
16481652
return err
16491653
}
16501654
}
1651-
if err := i.PushImageToReference(ctx, destRef, manifestType, "", "", "", writer, compress, SigningOptions{RemoveSignatures: removeSignatures}, &DockerRegistryOptions{}, additionaltags); err != nil {
1655+
if err := i.PushImageToReference(ctx, destRef, manifestType, "", "", "", writer, compress, SigningOptions{RemoveSignatures: removeSignatures}, &DockerRegistryOptions{}, additionaltags, nil); err != nil {
16521656
return errors.Wrapf(err, "unable to save %q", source)
16531657
}
16541658
i.newImageEvent(events.Save)

pkg/api/handlers/compat/images_push.go

Lines changed: 100 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package compat
22

33
import (
4+
"context"
5+
"encoding/json"
46
"fmt"
57
"io/ioutil"
68
"net/http"
@@ -10,11 +12,14 @@ import (
1012
"github.com/containers/podman/v3/libpod"
1113
"github.com/containers/podman/v3/pkg/api/handlers/utils"
1214
"github.com/containers/podman/v3/pkg/auth"
15+
"github.com/containers/podman/v3/pkg/channel"
1316
"github.com/containers/podman/v3/pkg/domain/entities"
1417
"github.com/containers/podman/v3/pkg/domain/infra/abi"
1518
"github.com/containers/storage"
19+
"github.com/docker/docker/pkg/jsonmessage"
1620
"github.com/gorilla/schema"
1721
"github.com/pkg/errors"
22+
"github.com/sirupsen/logrus"
1823
)
1924

2025
// PushImage is the handler for the compat http endpoint for pushing images.
@@ -82,6 +87,8 @@ func PushImage(w http.ResponseWriter, r *http.Request) {
8287
Password: password,
8388
Username: username,
8489
DigestFile: digestFile.Name(),
90+
Quiet: true,
91+
Progress: make(chan types.ProgressProperties),
8592
}
8693
if _, found := r.URL.Query()["tlsVerify"]; found {
8794
options.SkipTLSVerify = types.NewOptionalBool(!query.TLSVerify)
@@ -94,31 +101,103 @@ func PushImage(w http.ResponseWriter, r *http.Request) {
94101
destination = imageName
95102
}
96103

97-
if err := imageEngine.Push(r.Context(), imageName, destination, options); err != nil {
98-
if errors.Cause(err) != storage.ErrImageUnknown {
99-
utils.ImageNotFound(w, imageName, errors.Wrapf(err, "failed to find image %s", imageName))
100-
return
104+
errorWriter := channel.NewWriter(make(chan []byte))
105+
defer errorWriter.Close()
106+
107+
statusWriter := channel.NewWriter(make(chan []byte))
108+
defer statusWriter.Close()
109+
110+
runCtx, cancel := context.WithCancel(context.Background())
111+
var failed bool
112+
113+
go func() {
114+
defer cancel()
115+
116+
statusWriter.Write([]byte(fmt.Sprintf("The push refers to repository [%s]", imageName)))
117+
118+
err := imageEngine.Push(runCtx, imageName, destination, options)
119+
if err != nil {
120+
if errors.Cause(err) != storage.ErrImageUnknown {
121+
errorWriter.Write([]byte("An image does not exist locally with the tag: " + imageName))
122+
} else {
123+
errorWriter.Write([]byte(err.Error()))
124+
}
101125
}
126+
}()
102127

103-
utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "error pushing image %q", imageName))
104-
return
128+
flush := func() {
129+
if flusher, ok := w.(http.Flusher); ok {
130+
flusher.Flush()
131+
}
105132
}
106133

107-
digestBytes, err := ioutil.ReadAll(digestFile)
108-
if err != nil {
109-
utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "failed to read digest tmp file"))
110-
return
111-
}
134+
w.WriteHeader(http.StatusOK)
135+
w.Header().Add("Content-Type", "application/json")
136+
flush()
112137

113-
tag := query.Tag
114-
if tag == "" {
115-
tag = "latest"
116-
}
117-
respData := struct {
118-
Status string `json:"status"`
119-
}{
120-
Status: fmt.Sprintf("%s: digest: %s size: null", tag, string(digestBytes)),
121-
}
138+
enc := json.NewEncoder(w)
139+
enc.SetEscapeHTML(true)
140+
141+
loop: // break out of for/select infinite loop
142+
for {
143+
var report jsonmessage.JSONMessage
122144

123-
utils.WriteJSON(w, http.StatusOK, &respData)
145+
select {
146+
case e := <-options.Progress:
147+
switch e.Event {
148+
case types.ProgressEventNewArtifact:
149+
report.Status = "Preparing"
150+
case types.ProgressEventRead:
151+
report.Status = "Pushing"
152+
report.Progress = &jsonmessage.JSONProgress{
153+
Current: int64(e.Offset),
154+
Total: e.Artifact.Size,
155+
}
156+
case types.ProgressEventSkipped:
157+
report.Status = "Layer already exists"
158+
case types.ProgressEventDone:
159+
report.Status = "Pushed"
160+
}
161+
report.ID = e.Artifact.Digest.Encoded()[0:12]
162+
if err := enc.Encode(report); err != nil {
163+
errorWriter.Write([]byte(err.Error()))
164+
}
165+
flush()
166+
case e := <-statusWriter.Chan():
167+
report.Status = string(e)
168+
if err := enc.Encode(report); err != nil {
169+
errorWriter.Write([]byte(err.Error()))
170+
}
171+
flush()
172+
case e := <-errorWriter.Chan():
173+
failed = true
174+
report.Error = &jsonmessage.JSONError{
175+
Message: string(e),
176+
}
177+
report.ErrorMessage = string(e)
178+
if err := enc.Encode(report); err != nil {
179+
logrus.Warnf("Failed to json encode error %q", err.Error())
180+
}
181+
flush()
182+
case <-runCtx.Done():
183+
if !failed {
184+
digestBytes, err := ioutil.ReadAll(digestFile)
185+
if err == nil {
186+
tag := query.Tag
187+
if tag == "" {
188+
tag = "latest"
189+
}
190+
report.Status = fmt.Sprintf("%s: digest: %s", tag, string(digestBytes))
191+
if err := enc.Encode(report); err != nil {
192+
logrus.Warnf("Failed to json encode error %q", err.Error())
193+
}
194+
flush()
195+
}
196+
}
197+
break loop // break out of for/select infinite loop
198+
case <-r.Context().Done():
199+
// Client has closed connection
200+
break loop // break out of for/select infinite loop
201+
}
202+
}
124203
}

pkg/api/handlers/libpod/images.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,7 @@ func PushImage(w http.ResponseWriter, r *http.Request) {
451451
Password: password,
452452
Format: query.Format,
453453
All: query.All,
454+
Quiet: true,
454455
}
455456
if _, found := r.URL.Query()["tlsVerify"]; found {
456457
options.SkipTLSVerify = types.NewOptionalBool(!query.TLSVerify)

pkg/domain/entities/images.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ type ImagePushOptions struct {
203203
SignBy string
204204
// SkipTLSVerify to skip HTTPS and certificate verification.
205205
SkipTLSVerify types.OptionalBool
206+
// Progress to get progress notifications
207+
Progress chan types.ProgressProperties
206208
}
207209

208210
// ImageSearchOptions are the arguments for searching images.

pkg/domain/infra/abi/images.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,8 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri
376376
options.Compress,
377377
signOptions,
378378
&dockerRegistryOptions,
379-
nil)
379+
nil,
380+
options.Progress)
380381
if err != nil && errors.Cause(err) != storage.ErrImageUnknown {
381382
// Image might be a manifest list so attempt a manifest push
382383
if _, manifestErr := ir.ManifestPush(ctx, source, destination, options); manifestErr == nil {

test/apiv2/12-imagesMore.at

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ t POST "images/localhost:5000/myrepo/push?tlsVerify=false&tag=mytag" '' 200
4646
# Untag the image
4747
t POST "libpod/images/$iid/untag?repo=localhost:5000/myrepo&tag=mytag" '' 201
4848

49+
# Try to push non-existing image
50+
t POST "images/localhost:5000/idonotexist/push?tlsVerify=false" '' 200
51+
jq -re 'select(.errorDetail)' <<<"$output" &>/dev/null || echo -e "${red}not ok: error message not found in output${nc}" 1>&2
52+
4953
t GET libpod/images/$IMAGE/json 200 \
5054
.RepoTags[-1]=$IMAGE
5155

0 commit comments

Comments
 (0)