From 5ae9ecc4c7f4fd7c80ea6e38c87dc72fbc8e9f30 Mon Sep 17 00:00:00 2001 From: yufeng Date: Wed, 17 Sep 2025 15:43:25 +0800 Subject: [PATCH 1/2] feat: add --skip-verify flag to nerdctl save command Add --skip-verify flag to allow saving images when the original registry is unavailable but all layers are present locally. This addresses the issue where save operations fail due to network timeouts when trying to verify remote layers. Changes: - Add SkipVerify field to ImageSaveOptions type - Add --skip-verify command line flag with descriptive help text - Modify save logic to conditionally skip EnsureAllContent verification - Add informative log message when verification is skipped - Improve tag command success logging for better user feedback - Add basic test coverage for --skip-verify functionality Fixes issue where users cannot export locally tagged images when the original registry becomes inaccessible. --- cmd/nerdctl/image/image_save.go | 6 ++++++ cmd/nerdctl/image/image_save_test.go | 17 +++++++++++++++++ pkg/api/types/image_types.go | 2 ++ pkg/cmd/image/save.go | 11 ++++++++--- pkg/cmd/image/tag.go | 7 +++++-- 5 files changed, 38 insertions(+), 5 deletions(-) diff --git a/cmd/nerdctl/image/image_save.go b/cmd/nerdctl/image/image_save.go index 79c0c9cfd0a..16c8ee30951 100644 --- a/cmd/nerdctl/image/image_save.go +++ b/cmd/nerdctl/image/image_save.go @@ -42,6 +42,7 @@ func SaveCommand() *cobra.Command { SilenceErrors: true, } cmd.Flags().StringP("output", "o", "", "Write to a file, instead of STDOUT") + cmd.Flags().Bool("skip-verify", false, "Skip verification of remote layers. Use this when the original registry is unavailable but you have all layers locally.") // #region platform flags // platform is defined as StringSlice, not StringArray, to allow specifying "--platform=amd64,arm64" @@ -67,11 +68,16 @@ func saveOptions(cmd *cobra.Command) (types.ImageSaveOptions, error) { if err != nil { return types.ImageSaveOptions{}, err } + skipVerify, err := cmd.Flags().GetBool("skip-verify") + if err != nil { + return types.ImageSaveOptions{}, err + } return types.ImageSaveOptions{ GOptions: globalOptions, AllPlatforms: allPlatforms, Platform: platform, + SkipVerify: skipVerify, }, err } diff --git a/cmd/nerdctl/image/image_save_test.go b/cmd/nerdctl/image/image_save_test.go index 7b97f523761..391c0a10f9b 100644 --- a/cmd/nerdctl/image/image_save_test.go +++ b/cmd/nerdctl/image/image_save_test.go @@ -66,6 +66,23 @@ func TestSaveContent(t *testing.T) { testCase.Run(t) } +func TestSaveSkipVerify(t *testing.T) { + nerdtest.Setup() + + testCase := &test.Case{ + Require: require.Not(require.Windows), + Setup: func(data test.Data, helpers test.Helpers) { + helpers.Ensure("pull", "--quiet", testutil.CommonImage) + }, + Command: func(data test.Data, helpers test.Helpers) test.TestableCommand { + return helpers.Command("save", "--skip-verify", "-o", filepath.Join(data.Temp().Path(), "out.tar"), testutil.CommonImage) + }, + Expected: test.Expects(0, nil, nil), + } + + testCase.Run(t) +} + func TestSave(t *testing.T) { testCase := nerdtest.Setup() diff --git a/pkg/api/types/image_types.go b/pkg/api/types/image_types.go index 0ceb3148896..64f228f25d4 100644 --- a/pkg/api/types/image_types.go +++ b/pkg/api/types/image_types.go @@ -274,6 +274,8 @@ type ImageSaveOptions struct { AllPlatforms bool // Export content for a specific platform Platform []string + // Skip verification of remote layers + SkipVerify bool } // ImageSignOptions contains options for signing an image. It contains options from diff --git a/pkg/cmd/image/save.go b/pkg/cmd/image/save.go index 0a499b3f135..37ba30b8cfb 100644 --- a/pkg/cmd/image/save.go +++ b/pkg/cmd/image/save.go @@ -22,6 +22,7 @@ import ( containerd "github.com/containerd/containerd/v2/client" "github.com/containerd/containerd/v2/core/images/archive" + "github.com/containerd/log" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/idutil/imagewalker" @@ -50,9 +51,13 @@ func Save(ctx context.Context, client *containerd.Client, images []string, optio } // Ensure all the layers are here: https://github.com/containerd/nerdctl/issues/3425 - err = EnsureAllContent(ctx, client, found.Image.Name, platMC, options.GOptions) - if err != nil { - return err + if !options.SkipVerify { + err = EnsureAllContent(ctx, client, found.Image.Name, platMC, options.GOptions) + if err != nil { + return err + } + } else { + log.G(ctx).Info("Skipping remote layer verification (--skip-verify enabled)") } imgName := found.Image.Name diff --git a/pkg/cmd/image/tag.go b/pkg/cmd/image/tag.go index 60ab191d4f7..aace97b99f2 100644 --- a/pkg/cmd/image/tag.go +++ b/pkg/cmd/image/tag.go @@ -70,8 +70,8 @@ func Tag(ctx context.Context, client *containerd.Client, options types.ImageTagO err = EnsureAllContent(ctx, client, srcName, platMC, options.GOptions) if err != nil { - log.G(ctx).Warn("Unable to fetch missing layers before committing. " + - "If you try to save or push this image, it might fail. See https://github.com/containerd/nerdctl/issues/3439.") + log.G(ctx).Warn("Unable to verify all image layers are present locally (this does not affect the tag operation). " + + "If you later save or push this image, some layers may need to be re-downloaded. See https://github.com/containerd/nerdctl/issues/3439.") } img, err := imageService.Get(ctx, srcName) @@ -92,5 +92,8 @@ func Tag(ctx context.Context, client *containerd.Client, options types.ImageTagO return err } } + + // Log successful tag operation + log.G(ctx).Infof("Successfully tagged %s", parsedReference.String()) return nil } From 14f40463e591bdcb01d0ee566f313e9738b6e715 Mon Sep 17 00:00:00 2001 From: yufeng Date: Wed, 17 Sep 2025 15:56:23 +0800 Subject: [PATCH 2/2] fix gci error --- pkg/cmd/image/tag.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/cmd/image/tag.go b/pkg/cmd/image/tag.go index aace97b99f2..5301f8231b1 100644 --- a/pkg/cmd/image/tag.go +++ b/pkg/cmd/image/tag.go @@ -24,7 +24,6 @@ import ( "github.com/containerd/containerd/v2/core/images" "github.com/containerd/errdefs" "github.com/containerd/log" - "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/idutil/imagewalker" "github.com/containerd/nerdctl/v2/pkg/platformutil" @@ -92,7 +91,7 @@ func Tag(ctx context.Context, client *containerd.Client, options types.ImageTagO return err } } - + // Log successful tag operation log.G(ctx).Infof("Successfully tagged %s", parsedReference.String()) return nil