Skip to content

Commit d336353

Browse files
committed
libpod: add pullprogress to /libpod/images/pull
Following PR attempts to expose `pull progress` of images while a user attempts to pull image using `libpod`'s API endpoint `/images/pull` using a newly introduced flag called `pullprogress`. Output is similar to `compatMode` but not exactly same. Idea is to maintain a new endpoint which does not have to adhere with `docker` compatibility and can be extended for new features easily. Following flag will expose some new fields to pullprogress and will be slowly extended with more features. Signed-off-by: flouthoc <[email protected]>
1 parent ac5b9b0 commit d336353

File tree

4 files changed

+173
-8
lines changed

4 files changed

+173
-8
lines changed

pkg/api/handlers/libpod/images_pull.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,15 @@ func ImagesPull(w http.ResponseWriter, r *http.Request) {
3131
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
3232
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
3333
query := struct {
34-
AllTags bool `schema:"allTags"`
35-
CompatMode bool `schema:"compatMode"`
36-
PullPolicy string `schema:"policy"`
37-
Quiet bool `schema:"quiet"`
38-
Reference string `schema:"reference"`
39-
Retry uint `schema:"retry"`
40-
RetryDelay string `schema:"retrydelay"`
41-
TLSVerify bool `schema:"tlsVerify"`
34+
AllTags bool `schema:"allTags"`
35+
CompatMode bool `schema:"compatMode"`
36+
PullProgress bool `schema:"pullProgress"`
37+
PullPolicy string `schema:"policy"`
38+
Quiet bool `schema:"quiet"`
39+
Reference string `schema:"reference"`
40+
Retry uint `schema:"retry"`
41+
RetryDelay string `schema:"retrydelay"`
42+
TLSVerify bool `schema:"tlsVerify"`
4243
// Platform fields below:
4344
Arch string `schema:"Arch"`
4445
OS string `schema:"OS"`
@@ -129,6 +130,10 @@ func ImagesPull(w http.ResponseWriter, r *http.Request) {
129130
return
130131
}
131132

133+
if query.PullProgress {
134+
utils.PullProgress(r.Context(), w, runtime, query.Reference, pullPolicy, pullOptions)
135+
return
136+
}
132137
if query.CompatMode {
133138
utils.CompatPull(r.Context(), w, runtime, query.Reference, pullPolicy, pullOptions)
134139
return

pkg/api/handlers/utils/images.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/containers/image/v5/types"
1919
"github.com/containers/podman/v5/libpod"
2020
api "github.com/containers/podman/v5/pkg/api/types"
21+
entities "github.com/containers/podman/v5/pkg/domain/entities/types"
2122
"github.com/containers/podman/v5/pkg/errorhandling"
2223
"github.com/containers/storage"
2324
"github.com/docker/distribution/registry/api/errcode"
@@ -124,6 +125,100 @@ type pullResult struct {
124125
err error
125126
}
126127

128+
func PullProgress(ctx context.Context, w http.ResponseWriter, runtime *libpod.Runtime, reference string, pullPolicy config.PullPolicy, pullOptions *libimage.PullOptions) {
129+
progress := make(chan types.ProgressProperties)
130+
pullOptions.Progress = progress
131+
132+
pullResChan := make(chan pullResult)
133+
go func() {
134+
pulledImages, err := runtime.LibimageRuntime().Pull(ctx, reference, pullPolicy, pullOptions)
135+
pullResChan <- pullResult{images: pulledImages, err: err}
136+
}()
137+
138+
enc := json.NewEncoder(w)
139+
enc.SetEscapeHTML(true)
140+
141+
flush := func() {
142+
if flusher, ok := w.(http.Flusher); ok {
143+
flusher.Flush()
144+
}
145+
}
146+
147+
statusWritten := false
148+
writeStatusCode := func(code int) {
149+
if !statusWritten {
150+
w.WriteHeader(code)
151+
w.Header().Set("Content-Type", "application/json")
152+
flush()
153+
statusWritten = true
154+
}
155+
}
156+
157+
report := entities.ImagePullReportV2{}
158+
report.Status = "pulling"
159+
report.Stream = "Pulling image"
160+
161+
writeStatusCode(http.StatusOK)
162+
if err := enc.Encode(report); err != nil {
163+
logrus.Errorf("Error encoding pull report: %v", err)
164+
return
165+
}
166+
flush()
167+
168+
for {
169+
select {
170+
case e := <-progress:
171+
report.Status = "pulling"
172+
switch e.Event {
173+
case types.ProgressEventNewArtifact:
174+
report.Stream = "Pulling fs layer"
175+
case types.ProgressEventRead:
176+
report.Stream = "Downloading"
177+
report.Progress = &entities.ImagePullProgress{
178+
Current: e.Offset,
179+
Total: e.Artifact.Size,
180+
}
181+
case types.ProgressEventSkipped:
182+
report.Stream = "Layer already exists"
183+
case types.ProgressEventDone:
184+
report.Stream = "Pull complete"
185+
}
186+
if err := enc.Encode(report); err != nil {
187+
logrus.Errorf("Error encoding pull report: %v", err)
188+
return
189+
}
190+
flush()
191+
192+
case result := <-pullResChan:
193+
if result.err != nil {
194+
report.Status = "error"
195+
report.Stream = result.err.Error()
196+
writeStatusCode(http.StatusInternalServerError)
197+
if err := enc.Encode(report); err != nil {
198+
logrus.Errorf("Error encoding pull report: %v", err)
199+
}
200+
flush()
201+
return
202+
}
203+
204+
report.Status = "success"
205+
report.Stream = "Pull complete"
206+
images := []string{}
207+
for _, img := range result.images {
208+
images = append(images, img.ID())
209+
}
210+
211+
report.Images = images
212+
if err := enc.Encode(report); err != nil {
213+
logrus.Errorf("Error encoding pull report: %v", err)
214+
return
215+
}
216+
flush()
217+
return
218+
}
219+
}
220+
}
221+
127222
func CompatPull(ctx context.Context, w http.ResponseWriter, runtime *libpod.Runtime, reference string, pullPolicy config.PullPolicy, pullOptions *libimage.PullOptions) {
128223
progress := make(chan types.ProgressProperties)
129224
pullOptions.Progress = progress

pkg/domain/entities/types/images.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,53 @@ type ImagePullReport struct {
147147
ID string `json:"id,omitempty"`
148148
}
149149

150+
// ImagePullReportV2 provides a detailed status of a container image pull operation.
151+
type ImagePullReportV2 struct {
152+
// Status indicates the current state of the image pull operation.
153+
// Possible values are:
154+
// - "error": An error occurred during the pull.
155+
// - "pulling": The image pull is in progress.
156+
// - "success": The image pull completed successfully.
157+
Status string `json:"status,omitempty"`
158+
159+
// Images contains the unique identifiers of the successfully pulled images.
160+
// This field is only populated when the Status is "success".
161+
Images []string `json:"images,omitempty"`
162+
163+
// Stream provides a human-readable stream of events from the containers/image
164+
// library. This data is intended for display purposes and should not be parsed
165+
// by software.
166+
Stream string `json:"logStream,omitempty"`
167+
168+
// Progress provides detailed information about the download progress of an
169+
// image layer. This field is only populated when the Status is "pulling".
170+
Progress *ImagePullProgress `json:"progress,omitempty"`
171+
}
172+
173+
// ImagePullProgress details the download progress of a single image layer.
174+
type ImagePullProgress struct {
175+
// Current is the number of bytes of the current artifact that have been
176+
// downloaded so far.
177+
Current uint64 `json:"current,omitempty"`
178+
179+
// Total is the total size of the artifact in bytes. A value of -1
180+
// indicates that the total size is unknown.
181+
Total int64 `json:"total,omitempty"`
182+
183+
// Completed indicates whether the pull of the associated layer has finished.
184+
// This is particularly useful for tracking the status of "partial pulls"
185+
// where only a portion of the layers may be downloaded.
186+
Completed bool `json:"completed,omitempty"`
187+
188+
// LayerID is the unique identifier for the image layer being downloaded.
189+
LayerID string `json:"layerID,omitempty"`
190+
191+
// ProgressText is a human-readable string that represents the current
192+
// progress, often as a progress bar or status message. This text is for
193+
// display purposes only and should not be parsed.
194+
ProgressText string `json:"progressText,omitempty"`
195+
}
196+
150197
type ImagePushStream struct {
151198
// ManifestDigest is the digest of the manifest of the pushed image.
152199
ManifestDigest string `json:"manifestdigest,omitempty"`

test/apiv2/10-images.at

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,4 +440,22 @@ t GET "libpod/events?stream=false&since=$START" 200 \
440440
.Actor.Attributes.name="localhost:5000/idonotexist" \
441441
.Actor.Attributes.error~".*connection refused"
442442

443+
# Test pullProgress parameter on /libpod/images/pull endpoint
444+
# Remove the image first to ensure we actually pull it
445+
podman rmi -f quay.io/libpod/alpine:latest >/dev/null 2>&1 || true
446+
447+
# Test pullProgress=true - should return ImagePullReportV2 format with progress info
448+
t POST "libpod/images/pull?reference=quay.io/libpod/alpine:latest&pullProgress=true" 200 \
449+
.status~".*success.*" \
450+
.logStream~".*Pull complete.*"
451+
452+
453+
# Test pullProgress=true with invalid image - should return error status
454+
t POST "libpod/images/pull?reference=localhost:5000/idonotexist:latest&pullProgress=true" 200 \
455+
.status~".*error.*" \
456+
.logStream~".*"
457+
458+
# Clean up
459+
podman rmi -f quay.io/libpod/alpine:latest >/dev/null 2>&1 || true
460+
443461
# vim: filetype=sh

0 commit comments

Comments
 (0)