From d8e9b7f97d6cf3de6da80235c1293781311a05ab Mon Sep 17 00:00:00 2001 From: root Date: Mon, 15 Dec 2025 21:00:21 -0600 Subject: [PATCH] image convert: add progress reportng, live updating still in progress --- cmd/nerdctl/image/image_convert.go | 6 ++-- pkg/api/types/image_types.go | 5 +-- pkg/cmd/image/convert.go | 58 ++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/cmd/nerdctl/image/image_convert.go b/cmd/nerdctl/image/image_convert.go index ebe86119e31..da533dae5ce 100644 --- a/cmd/nerdctl/image/image_convert.go +++ b/cmd/nerdctl/image/image_convert.go @@ -116,6 +116,7 @@ func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) { if err != nil { return types.ImageConvertOptions{}, err } + progressOutput := cmd.ErrOrStderr() format, err := cmd.Flags().GetString("format") if err != nil { return types.ImageConvertOptions{}, err @@ -261,8 +262,9 @@ func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) { } // #endregion return types.ImageConvertOptions{ - GOptions: globalOptions, - Format: format, + GOptions: globalOptions, + Format: format, + ProgressOutput: progressOutput, // #region generic flags Uncompress: uncompress, Oci: oci, diff --git a/pkg/api/types/image_types.go b/pkg/api/types/image_types.go index 8999d659213..29ba08607d9 100644 --- a/pkg/api/types/image_types.go +++ b/pkg/api/types/image_types.go @@ -47,8 +47,9 @@ type ImageListOptions struct { // ImageConvertOptions specifies options for `nerdctl image convert`. type ImageConvertOptions struct { - Stdout io.Writer - GOptions GlobalCommandOptions + Stdout io.Writer + ProgressOutput io.Writer + GOptions GlobalCommandOptions // #region generic flags // Uncompress convert tar.gz layers to uncompressed tar layers diff --git a/pkg/cmd/image/convert.go b/pkg/cmd/image/convert.go index df022b90011..25d3737fab3 100644 --- a/pkg/cmd/image/convert.go +++ b/pkg/cmd/image/convert.go @@ -45,10 +45,13 @@ import ( "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" + "github.com/containerd/nerdctl/v2/pkg/containerdutil" converterutil "github.com/containerd/nerdctl/v2/pkg/imgutil/converter" + "github.com/containerd/nerdctl/v2/pkg/imgutil/jobs" "github.com/containerd/nerdctl/v2/pkg/platformutil" "github.com/containerd/nerdctl/v2/pkg/referenceutil" "github.com/containerd/nerdctl/v2/pkg/snapshotterutil" + "github.com/containerd/platforms" ) func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRawRef string, options types.ImageConvertOptions) error { @@ -205,11 +208,40 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa convertOpts = append(convertOpts, converter.WithDockerToOCI(true)) } + var ( + cancelProgress context.CancelFunc + progressDone chan struct{} + ) + + if options.ProgressOutput != nil { + var progressCtx context.Context + progressCtx, cancelProgress = context.WithCancel(ctx) + progressDone = make(chan struct{}) + ongoing := jobs.New(targetRef) + if err := addDescriptorsToJobs(ctx, client, srcRef, platMC, ongoing); err != nil { + return err + } + go func() { + jobs.ShowProgress(progressCtx, ongoing, client.ContentStore(), options.ProgressOutput) + close(progressDone) + }() + } + // converter.Convert() gains the lease by itself newImg, err := converterutil.Convert(ctx, client, targetRef, srcRef, convertOpts...) if err != nil { + if cancelProgress != nil { + cancelProgress() + <-progressDone + } return err } + + if cancelProgress != nil { + cancelProgress() + <-progressDone + } + res := converterutil.ConvertedImageInfo{ Image: newImg.Name + "@" + newImg.Target.Digest.String(), } @@ -233,6 +265,32 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa return printConvertedImage(options.Stdout, options, res) } +func addDescriptorsToJobs(ctx context.Context, client *containerd.Client, srcRef string, platMC platforms.MatchComparer, ongoing *jobs.Jobs) error { + imageService := client.ImageService() + img, err := imageService.Get(ctx, srcRef) + if err != nil { + return err + } + provider := containerdutil.NewProvider(client) + handler := images.ChildrenHandler(provider) + if platMC != nil { + handler = images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { + if desc.Platform != nil && !platMC.Match(*desc.Platform) { + return nil, nil + } + return images.Children(ctx, provider, desc) + }) + } + err = images.Walk(ctx, images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { + ongoing.Add(desc) + return handler(ctx, desc) + }), img.Target) + if err != nil { + return err + } + return nil +} + func getESGZConverter(options types.ImageConvertOptions) (convertFunc converter.ConvertFunc, finalize func(ctx context.Context, cs content.Store, ref string, desc *ocispec.Descriptor) (*images.Image, error), _ error) { if options.EstargzExternalToc && !options.GOptions.Experimental { return nil, nil, fmt.Errorf("estargz-external-toc requires experimental mode to be enabled")