Skip to content

libpod: add pullprogress to /libpod/images/pull #26444

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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
21 changes: 13 additions & 8 deletions pkg/api/handlers/libpod/images_pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,15 @@ 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 {
AllTags bool `schema:"allTags"`
CompatMode bool `schema:"compatMode"`
PullPolicy string `schema:"policy"`
Quiet bool `schema:"quiet"`
Reference string `schema:"reference"`
Retry uint `schema:"retry"`
RetryDelay string `schema:"retrydelay"`
TLSVerify bool `schema:"tlsVerify"`
AllTags bool `schema:"allTags"`
CompatMode bool `schema:"compatMode"`
PullProgress bool `schema:"pullProgress"`
PullPolicy string `schema:"policy"`
Quiet bool `schema:"quiet"`
Reference string `schema:"reference"`
Retry uint `schema:"retry"`
RetryDelay string `schema:"retrydelay"`
TLSVerify bool `schema:"tlsVerify"`
// Platform fields below:
Arch string `schema:"Arch"`
OS string `schema:"OS"`
Expand Down Expand Up @@ -129,6 +130,10 @@ func ImagesPull(w http.ResponseWriter, r *http.Request) {
return
}

if query.PullProgress {
utils.PullProgress(r.Context(), w, runtime, query.Reference, pullPolicy, pullOptions)
return
}
if query.CompatMode {
utils.CompatPull(r.Context(), w, runtime, query.Reference, pullPolicy, pullOptions)
return
Expand Down
101 changes: 101 additions & 0 deletions pkg/api/handlers/utils/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/containers/image/v5/types"
"github.com/containers/podman/v5/libpod"
api "github.com/containers/podman/v5/pkg/api/types"
entities "github.com/containers/podman/v5/pkg/domain/entities/types"
"github.com/containers/podman/v5/pkg/errorhandling"
"github.com/containers/storage"
"github.com/docker/distribution/registry/api/errcode"
Expand Down Expand Up @@ -124,6 +125,106 @@ type pullResult struct {
err error
}

func PullProgress(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
}
}

report := entities.ImagePullReportV2{}
report.Status = "pulling"
report.Stream = "Pulling image"

writeStatusCode(http.StatusOK)
if err := enc.Encode(report); err != nil {
logrus.Errorf("Error encoding pull report: %v", err)
return
}
flush()

for {
select {
case e := <-progress:
report.Status = "pulling"
switch e.Event {
case types.ProgressEventNewArtifact:
report.Stream = "Pulling fs layer"
case types.ProgressEventRead:
report.Stream = "Downloading"
report.Progress = &entities.ImagePullProgress{
Current: e.Offset,
Total: e.Artifact.Size,
}
case types.ProgressEventSkipped:
report.Stream = "Layer already exists"
case types.ProgressEventDone:
report.Stream = "Pull complete"
report.Progress = &entities.ImagePullProgress{
Current: e.Offset,
Total: e.Artifact.Size,
ProgressComponentID: e.Artifact.Digest.String(),
Completed: true,
}
}
if err := enc.Encode(report); err != nil {
logrus.Errorf("Error encoding pull report: %v", err)
return
}
flush()

case result := <-pullResChan:
if result.err != nil {
report.Status = "error"
report.Stream = result.err.Error()
writeStatusCode(http.StatusInternalServerError)
if err := enc.Encode(report); err != nil {
logrus.Errorf("Error encoding pull report: %v", err)
}
flush()
return
}

report.Status = "success"
report.Stream = "Pull complete"
images := []string{}
for _, img := range result.images {
images = append(images, img.ID())
}

report.Images = images
if err := enc.Encode(report); err != nil {
logrus.Errorf("Error encoding pull report: %v", err)
return
}
flush()
return
}
}
}

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
Expand Down
51 changes: 51 additions & 0 deletions pkg/domain/entities/types/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,57 @@ type ImagePullReport struct {
ID string `json:"id,omitempty"`
}

// ImagePullReportV2 provides a detailed status of a container image pull operation.
type ImagePullReportV2 struct {
// Status indicates the current state of the image pull operation.
// Possible values are:
// - "error": An error occurred during the pull.
// - "pulling": The image pull is in progress.
// - "success": The image pull completed successfully.
Status string `json:"status,omitempty"`

// Images contains the unique identifiers of the successfully pulled images.
// This field is only populated when the Status is "success".
Images []string `json:"images,omitempty"`

// Stream provides a human-readable stream of events from the containers/image
// library. This data is intended for display purposes and should not be parsed
// by software.
Stream string `json:"stream,omitempty"`

// Progress provides detailed information about the download progress of an
// image layer. This field is only populated when the Status is "pulling".
Progress *ImagePullProgress `json:"progress,omitempty"`
}

// ImagePullProgress details the download progress of a single image layer.
type ImagePullProgress struct {
// Current is the number of bytes of the current artifact that have been
// downloaded so far.
Current uint64 `json:"current,omitempty"`

// Total is the total size of the artifact in bytes. A value of -1
// indicates that the total size is unknown.
Total int64 `json:"total,omitempty"`

// Completed indicates whether the pull of the associated layer has finished.
// This is particularly useful for tracking the status of "partial pulls"
// where only a portion of the layers may be downloaded.
Completed bool `json:"completed,omitempty"`

// ProgressComponentID is the unique identifier for the artifact being downloaded.
// When the status is `PullComplete`, this field will contain the digest of the blobs at the source.
// If the status is `Success`, indicating that the entire PullOperation is complete,
// this field will contain the digest or ID of the artifact in local storage.
// Note: The values in this field may change in future updates of podman.
ProgressComponentID string `json:"progressComponentID,omitempty"`

// ProgressText is a human-readable string that represents the current
// progress, often as a progress bar or status message. This text is for
// display purposes only and should not be parsed.
ProgressText string `json:"progressText,omitempty"`
}

type ImagePushStream struct {
// ManifestDigest is the digest of the manifest of the pushed image.
ManifestDigest string `json:"manifestdigest,omitempty"`
Expand Down
39 changes: 39 additions & 0 deletions test/apiv2/10-images.at
Original file line number Diff line number Diff line change
Expand Up @@ -440,4 +440,43 @@ t GET "libpod/events?stream=false&since=$START" 200 \
.Actor.Attributes.name="localhost:5000/idonotexist" \
.Actor.Attributes.error~".*connection refused"

# Test pullProgress parameter on /libpod/images/pull endpoint
# Remove the image first to ensure we actually pull it
podman rmi -f quay.io/libpod/alpine:latest >/dev/null 2>&1 || true

# Test pullProgress=true - should return ImagePullReportV2 format with progress info
t POST "libpod/images/pull?reference=quay.io/libpod/alpine:latest&pullProgress=true" 200 \
.status~".*success.*" \
.stream~".*Pull complete.*"


# Test pullProgress=true with invalid image - should return error status
t POST "libpod/images/pull?reference=localhost:5000/idonotexist:latest&pullProgress=true" 200 \
.status~".*error.*" \
.stream~".*"

# Test pullProgress=true shows layer IDs during pull
# Remove the image first to ensure we actually pull it
podman rmi -f quay.io/libpod/alpine:latest >/dev/null 2>&1 || true

# Get layer digests using skopeo inspect
layer_digests=$(skopeo inspect docker://quay.io/libpod/alpine:latest | jq -r '.Layers[]' | sed 's/sha256://' | head -5)

# Pull image with pullProgress=true and verify layer IDs are in the stream
t POST "libpod/images/pull?reference=quay.io/libpod/alpine:latest&pullProgress=true" 200 \
.status~".*success.*" \
.stream~".*Pull complete.*"

# Verify each layer digest appears in the stream output
for layer_id in $layer_digests; do
if [[ $output =~ $layer_id ]]; then
_show_ok 1 "pullProgress shows layer ID $layer_id"
else
_show_ok 0 "pullProgress shows layer ID $layer_id" "[layer ID $layer_id not found in output]"
fi
done

# Clean up
podman rmi -f quay.io/libpod/alpine:latest >/dev/null 2>&1 || true

# vim: filetype=sh