copy: Add text-based progress output for non-TTY environments (skopeo#658)#665
copy: Add text-based progress output for non-TTY environments (skopeo#658)#665oshe-gi wants to merge 2 commits intocontainers:mainfrom
Conversation
4879e54 to
6f11c00
Compare
6f11c00 to
7feabb7
Compare
3d21a59 to
dc20b30
Compare
image/copy/progress_nontty.go
Outdated
| // setupNonTTYProgressWriter configures text-based progress output for non-TTY | ||
| // environments unless the caller already provided a buffered Progress channel. | ||
| // Returns a cleanup function that must be deferred by the caller. | ||
| // It relies on I dea that options.Progress channel is ony used once, to track progress with progress bar |
There was a problem hiding this comment.
| // It relies on I dea that options.Progress channel is ony used once, to track progress with progress bar | |
| // It relies on the idea that options.Progress channel is only used once, to track progress with a progress bar |
image/copy/progress_nontty.go
Outdated
| // environments unless the caller already provided a buffered Progress channel. | ||
| // Returns a cleanup function that must be deferred by the caller. | ||
| // It relies on I dea that options.Progress channel is ony used once, to track progress with progress bar | ||
| // Otherwize we must do some sort of fan-out |
There was a problem hiding this comment.
| // Otherwize we must do some sort of fan-out | |
| // Otherwise we must do some sort of fan-out |
|
@oshe-gi, your test code needs some formatting to pass our lint tests. try |
|
Thanks for comments @TomSweeneyRedHat , done. |
dde7dd7 to
6b03cf7
Compare
|
@mtrmac the Skopeo tests is failing with: Do we need to vendor the latest c/image into Skopeo and create a release to make this test happy again? |
|
Changes LGTM once we can make the tests happy. I don't think they're failing due to these changes. |
Just rebasing this PR on top of current |
When copying images in non-TTY environments (CI/CD pipelines, redirected
output, piped commands), the visual mpb progress bars are discarded,
leaving users with no visibility into transfer progress. This makes it
difficult to detect stalled transfers or monitor long-running copies.
This change adds a nonTTYProgressWriter that consumes progress events
from the existing Progress channel and prints periodic aggregate progress
lines suitable for log output:
Progress: 13.1 MiB / 52.3 MiB
Progress: 26.2 MiB / 52.3 MiB
Progress: 52.3 MiB / 52.3 MiB
The feature is enabled when, output is not a TTY, we check if option.Progress is set,
otherwise create a new buffered channel for progress events.
Note: Unbuffered channels are replaced with buffered ones to prevent
blocking during parallel blob downloads. Callers who need custom
consumption should provide a properly buffered channel.
Relates-to: containers/skopeo#658
Signed-off-by: Oleksandr Shestopal <ar.shestopal-oshegithub@gmail.com>
6b03cf7 to
e5f2e34
Compare
|
@mtrmac @TomSweeneyRedHat rebased on latest main, please approve validation CI |
mtrmac
left a comment
There was a problem hiding this comment.
Thanks!
Just a fairly brief look now.
image/copy/copy.go
Outdated
| // If reportWriter is not a TTY (e.g., when piping to a file), do not | ||
| // print the progress bars to avoid long and hard to parse output. | ||
| // Instead use printCopyInfo() to print single line "Copying ..." messages. | ||
| // Instead use text-based aggregate progress via nonTTYProgressWriter. |
There was a problem hiding this comment.
None of the existing output to reportWriter was removed; that’s probably good but the comment is now misleading.
image/copy/progress_nontty.go
Outdated
|
|
||
| // setupNonTTYProgressWriter configures text-based progress output for non-TTY | ||
| // environments unless the caller already provided a buffered Progress channel. | ||
| // Returns a cleanup function that must be deferred by the caller. |
image/copy/progress_nontty.go
Outdated
| options.ProgressInterval = nonTTYProgressInterval | ||
| } | ||
|
|
||
| if options.Progress == nil || cap(options.Progress) == 0 { |
There was a problem hiding this comment.
What does cap have to do with anything?!
There was a problem hiding this comment.
I suppose options.Progress to be buffered to prevent blocking.
There was a problem hiding this comment.
If the caller of copy.Image does submit a progress channel but doesn’t make it buffered for whatever reason (confident in not blocking the copy, or just forgot), I don’t think that’s a reason to entirely ignore the caller’s progress channel and to use the text progress reporting instead.
image/copy/progress_nontty.go
Outdated
| // This allows users to slow down output while maintaining a sensible minimum. | ||
| interval := max(options.ProgressInterval, nonTTYProgressInterval) | ||
| if options.ProgressInterval <= 0 { | ||
| options.ProgressInterval = nonTTYProgressInterval |
There was a problem hiding this comment.
Why does this touch the option if it is not going to consume the channel?
And if it is going to consume the channel, why does it make sense to set the consumers’ and the producers’ intervals to different values? (Should there even be two?)
There was a problem hiding this comment.
With interval user can change how often he want to see the update, if updates are too often, it will produce long output in non-TTY. If it is not provided or too small - set the default.
There was a problem hiding this comment.
I did not find where we set ProgressInterval as well
There was a problem hiding this comment.
c/image/copy.Image is a public API; it has primary callers in Podman and friends, but there are users outside of the GitHub.com/containers namespace.
There was a problem hiding this comment.
So then I think we need to make sure we initialize ProgressInterval and Progress channel in skopeo if nonTTY and no --quiet option is set.
| // aggregate progress. Blocks until the channel is closed. Intended to | ||
| // be called as a goroutine: go tw.Run(progressChan) | ||
| func (w *nonTTYProgressWriter) Run() { | ||
| for props := range w.progressChannel { |
There was a problem hiding this comment.
This is not going to ever terminate, AFAICS
| switch props.Event { | ||
| case types.ProgressEventNewArtifact: | ||
| // New blob starting - add its size to total | ||
| w.totalSize += props.Artifact.Size |
There was a problem hiding this comment.
That’s not a very useful concept of a “total progress” if the total is increasing over time. And sometimes we trigger an upload but then never consume all of the source; if this is not watching ProgressEventDone, it might never reach 100%.
How does this interact with multi-platform images?
(Working one platform at a time would be easier in that we usually (not always!) know the size of the input in advance.)
There was a problem hiding this comment.
The Idea of total progress is to give at least rough estimate, and short output.
What we can do is to wait when all parallel copying have started, and then start computing the progress.
Do you want to track per-blob progress?
Currently It will stop when copying is done, but I can work on processing ProgressEventDone events as well.
I have tested with golang image, there is an example in PR description. Is it a valid test?
There was a problem hiding this comment.
@mtrmac this is how I tested.
--policy ./default-policy.json \
--override-os linux \
--override-arch amd64 \
copy \
--image-parallel-copies 8 \
docker://docker.io/library/golang:1.24-bookworm \
dir:/tmp/golang-copy | catCould you provide better/more ways to test?
There was a problem hiding this comment.
What we can do is to wait when all parallel copying have started, and then start computing the progress.
We don’t start all layer copies simultaneously. Try a 20-layer image (large enough, or on a network slow enough, to be able to observe the progress.). Compare maxParallelDownloads.
We read the manifest before starting copies, and that usually (not always!) contains all of the layers’ sizes for the per-platform instance.
There was a problem hiding this comment.
docker://docker.io/library/golang:1.24-bookworm is a reasonable image, but try copy --all
…ProgressInterval Relates-to: containers/skopeo#658 Signed-off-by: Oleksandr Shestopal <ar.shestopal-oshegithub@gmail.com>
Relates-to: containers/skopeo#658
Problem
When copying images in non-TTY environments (CI/CD pipelines, redirected output, piped commands), the visual mpb progress bars are discarded via
io.Discard, leaving users with no visibility into transfer progress. This makes it difficult to:Currently, only a single "Copying blob..." message is printed via
printCopyInfo()when non-TTY, with no subsequent progress updates.Solution
This change automatically enables text-based progress output for non-TTY environments by leveraging the existing
Options.Progresschannel mechanism.Design decisions
Automatic for non-TTY: When output is not a TTY and the caller hasn't already provided a buffered Progress channel, we automatically set up text-based progress output. No opt-in flag needed.
Aggregate progress instead of per-blob: Rather than printing a line for each blob (which would be verbose for multi-layer images), we track total bytes across all blobs and print a single aggregate progress line. This keeps CI logs clean while still providing visibility.
Reuse existing Progress channel: The
progressReaderinblob.goalready sendsProgressEventNewArtifact,ProgressEventRead, andProgressEventDoneevents. We simply consume these events and format them as text output.Buffered channel: We create a buffered channel to prevent blocking senders during parallel blob downloads. Callers who need custom consumption should provide a properly buffered channel.
Sensible interval defaults: If
ProgressIntervalis not set, we default to 500ms. If the caller sets a larger interval, we respect that. The interval is clamped to a minimum of 500ms to prevent log spam.Output example
Files changed
copy/progress_nontty.go- New file withnonTTYProgressWriterimplementationcopy/copy.go- Initialize text progress writer in non-TTY modeTesting
Build and unit tests:
Manual testing with skopeo (pipe to cat to force non-TTY):
Output may be shorter, if ProgressInterval is set longer.
Signed-off-by: Oleksandr Shestopal ar.shestopal@gmail.com