Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compat API: Fix the response of 'push image' endpoint #9647

Merged
merged 1 commit into from
Mar 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions libpod/image/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -816,7 +816,7 @@ func (i *Image) UntagImage(tag string) error {

// PushImageToHeuristicDestination pushes the given image to "destination", which is heuristically parsed.
// Use PushImageToReference if the destination is known precisely.
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 {
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 {
if destination == "" {
return errors.Wrapf(syscall.EINVAL, "destination image name must be specified")
}
Expand All @@ -834,11 +834,11 @@ func (i *Image) PushImageToHeuristicDestination(ctx context.Context, destination
return err
}
}
return i.PushImageToReference(ctx, dest, manifestMIMEType, authFile, digestFile, signaturePolicyPath, writer, forceCompress, signingOptions, dockerRegistryOptions, additionalDockerArchiveTags)
return i.PushImageToReference(ctx, dest, manifestMIMEType, authFile, digestFile, signaturePolicyPath, writer, forceCompress, signingOptions, dockerRegistryOptions, additionalDockerArchiveTags, progress)
}

// PushImageToReference pushes the given image to a location described by the given path
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 {
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 {
sc := GetSystemContext(signaturePolicyPath, authFile, forceCompress)
sc.BlobInfoCacheDir = filepath.Join(i.imageruntime.store.GraphRoot(), "cache")

Expand All @@ -859,6 +859,10 @@ func (i *Image) PushImageToReference(ctx context.Context, dest types.ImageRefere
}
copyOptions := getCopyOptions(sc, writer, nil, dockerRegistryOptions, signingOptions, manifestMIMEType, additionalDockerArchiveTags)
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.
if progress != nil {
copyOptions.Progress = progress
copyOptions.ProgressInterval = time.Second
}
// Copy the image to the remote destination
manifestBytes, err := cp.Image(ctx, policyContext, dest, src, copyOptions)
if err != nil {
Expand Down Expand Up @@ -1648,7 +1652,7 @@ func (i *Image) Save(ctx context.Context, source, format, output string, moreTag
return err
}
}
if err := i.PushImageToReference(ctx, destRef, manifestType, "", "", "", writer, compress, SigningOptions{RemoveSignatures: removeSignatures}, &DockerRegistryOptions{}, additionaltags); err != nil {
if err := i.PushImageToReference(ctx, destRef, manifestType, "", "", "", writer, compress, SigningOptions{RemoveSignatures: removeSignatures}, &DockerRegistryOptions{}, additionaltags, nil); err != nil {
return errors.Wrapf(err, "unable to save %q", source)
}
i.newImageEvent(events.Save)
Expand Down
121 changes: 100 additions & 21 deletions pkg/api/handlers/compat/images_push.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package compat

import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
Expand All @@ -10,11 +12,14 @@ import (
"github.com/containers/podman/v3/libpod"
"github.com/containers/podman/v3/pkg/api/handlers/utils"
"github.com/containers/podman/v3/pkg/auth"
"github.com/containers/podman/v3/pkg/channel"
"github.com/containers/podman/v3/pkg/domain/entities"
"github.com/containers/podman/v3/pkg/domain/infra/abi"
"github.com/containers/storage"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/gorilla/schema"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)

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

if err := imageEngine.Push(r.Context(), imageName, destination, options); err != nil {
if errors.Cause(err) != storage.ErrImageUnknown {
utils.ImageNotFound(w, imageName, errors.Wrapf(err, "failed to find image %s", imageName))
return
errorWriter := channel.NewWriter(make(chan []byte))
defer errorWriter.Close()

statusWriter := channel.NewWriter(make(chan []byte))
defer statusWriter.Close()

runCtx, cancel := context.WithCancel(context.Background())
var failed bool

go func() {
defer cancel()

statusWriter.Write([]byte(fmt.Sprintf("The push refers to repository [%s]", imageName)))

err := imageEngine.Push(runCtx, imageName, destination, options)
if err != nil {
if errors.Cause(err) != storage.ErrImageUnknown {
errorWriter.Write([]byte("An image does not exist locally with the tag: " + imageName))
} else {
errorWriter.Write([]byte(err.Error()))
}
}
}()

utils.Error(w, "Something went wrong.", http.StatusBadRequest, errors.Wrapf(err, "error pushing image %q", imageName))
return
flush := func() {
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
}

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

tag := query.Tag
if tag == "" {
tag = "latest"
}
respData := struct {
Status string `json:"status"`
}{
Status: fmt.Sprintf("%s: digest: %s size: null", tag, string(digestBytes)),
}
enc := json.NewEncoder(w)
enc.SetEscapeHTML(true)

loop: // break out of for/select infinite loop
for {
var report jsonmessage.JSONMessage

utils.WriteJSON(w, http.StatusOK, &respData)
select {
case e := <-options.Progress:
switch e.Event {
case types.ProgressEventNewArtifact:
report.Status = "Preparing"
case types.ProgressEventRead:
report.Status = "Pushing"
report.Progress = &jsonmessage.JSONProgress{
Current: int64(e.Offset),
Total: e.Artifact.Size,
}
case types.ProgressEventSkipped:
report.Status = "Layer already exists"
case types.ProgressEventDone:
report.Status = "Pushed"
}
report.ID = e.Artifact.Digest.Encoded()[0:12]
if err := enc.Encode(report); err != nil {
errorWriter.Write([]byte(err.Error()))
}
flush()
case e := <-statusWriter.Chan():
report.Status = string(e)
if err := enc.Encode(report); err != nil {
errorWriter.Write([]byte(err.Error()))
}
flush()
case e := <-errorWriter.Chan():
failed = true
report.Error = &jsonmessage.JSONError{
Message: string(e),
}
report.ErrorMessage = string(e)
if err := enc.Encode(report); err != nil {
logrus.Warnf("Failed to json encode error %q", err.Error())
}
flush()
case <-runCtx.Done():
if !failed {
digestBytes, err := ioutil.ReadAll(digestFile)
if err == nil {
tag := query.Tag
if tag == "" {
tag = "latest"
}
report.Status = fmt.Sprintf("%s: digest: %s", tag, string(digestBytes))
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
case <-r.Context().Done():
// Client has closed connection
break loop // break out of for/select infinite loop
}
}
}
1 change: 1 addition & 0 deletions pkg/api/handlers/libpod/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@ func PushImage(w http.ResponseWriter, r *http.Request) {
Password: password,
Format: query.Format,
All: query.All,
Quiet: true,
}
if _, found := r.URL.Query()["tlsVerify"]; found {
options.SkipTLSVerify = types.NewOptionalBool(!query.TLSVerify)
Expand Down
2 changes: 2 additions & 0 deletions pkg/domain/entities/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ type ImagePushOptions struct {
SignBy string
// SkipTLSVerify to skip HTTPS and certificate verification.
SkipTLSVerify types.OptionalBool
// Progress to get progress notifications
Progress chan types.ProgressProperties
}

// ImageSearchOptions are the arguments for searching images.
Expand Down
3 changes: 2 additions & 1 deletion pkg/domain/infra/abi/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -376,7 +376,8 @@ func (ir *ImageEngine) Push(ctx context.Context, source string, destination stri
options.Compress,
signOptions,
&dockerRegistryOptions,
nil)
nil,
options.Progress)
if err != nil && errors.Cause(err) != storage.ErrImageUnknown {
// Image might be a manifest list so attempt a manifest push
if _, manifestErr := ir.ManifestPush(ctx, source, destination, options); manifestErr == nil {
Expand Down
4 changes: 4 additions & 0 deletions test/apiv2/12-imagesMore.at
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ t POST "images/localhost:5000/myrepo/push?tlsVerify=false&tag=mytag" '' 200
# Untag the image
t POST "libpod/images/$iid/untag?repo=localhost:5000/myrepo&tag=mytag" '' 201

# Try to push non-existing image
t POST "images/localhost:5000/idonotexist/push?tlsVerify=false" '' 200
jq -re 'select(.errorDetail)' <<<"$output" &>/dev/null || echo -e "${red}not ok: error message not found in output${nc}" 1>&2

t GET libpod/images/$IMAGE/json 200 \
.RepoTags[-1]=$IMAGE

Expand Down