diff --git a/cmd/nerdctl/image/image_save.go b/cmd/nerdctl/image/image_save.go index 89a1eac1e5a..61bcbab54a3 100644 --- a/cmd/nerdctl/image/image_save.go +++ b/cmd/nerdctl/image/image_save.go @@ -45,6 +45,7 @@ func SaveCommand() *cobra.Command { SilenceErrors: true, } cmd.Flags().StringP("output", "o", "", "Write to a file, instead of STDOUT") + cmd.Flags().BoolP("quiet", "q", false, "Suppress the progress output") // #region platform flags // platform is defined as StringSlice, not StringArray, to allow specifying "--platform=amd64,arm64" @@ -70,11 +71,16 @@ func saveOptions(cmd *cobra.Command) (types.ImageSaveOptions, error) { if err != nil { return types.ImageSaveOptions{}, err } + quiet, err := cmd.Flags().GetBool("quiet") + if err != nil { + return types.ImageSaveOptions{}, err + } return types.ImageSaveOptions{ GOptions: globalOptions, AllPlatforms: allPlatforms, Platform: platform, + Quiet: quiet, }, err } diff --git a/cmd/nerdctl/image/image_save_test.go b/cmd/nerdctl/image/image_save_test.go index 7b97f523761..7671570d31c 100644 --- a/cmd/nerdctl/image/image_save_test.go +++ b/cmd/nerdctl/image/image_save_test.go @@ -66,6 +66,37 @@ func TestSaveContent(t *testing.T) { testCase.Run(t) } +func TestSaveQuiet(t *testing.T) { + nerdtest.Setup() + + testCase := &test.Case{ + // --quiet is a nerdctl-specific flag, so this is skipped under the Docker compatibility mode. + Require: require.All(require.Not(require.Windows), require.Not(nerdtest.Docker)), + Setup: func(_ 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", "--quiet", "-o", filepath.Join(data.Temp().Path(), "out.tar"), testutil.CommonImage) + }, + Expected: func(data test.Data, _ test.Helpers) *test.Expected { + return &test.Expected{ + ExitCode: expect.ExitCodeSuccess, + Output: func(_ string, t tig.T) { + // The archive is still written correctly with --quiet. + rootfsPath := filepath.Join(data.Temp().Path(), "rootfs") + err := testhelpers.ExtractDockerArchive(filepath.Join(data.Temp().Path(), "out.tar"), rootfsPath) + assert.NilError(t, err) + etcOSReleaseBytes, err := os.ReadFile(filepath.Join(rootfsPath, "/etc/os-release")) + assert.NilError(t, err) + assert.Assert(t, strings.Contains(string(etcOSReleaseBytes), "Alpine")) + }, + } + }, + } + + testCase.Run(t) +} + func TestSave(t *testing.T) { testCase := nerdtest.Setup() diff --git a/docs/command-reference.md b/docs/command-reference.md index 93c481b4c11..d0c18183b22 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -928,6 +928,7 @@ Usage: `nerdctl save [OPTIONS] IMAGE [IMAGE...]` Flags: - :whale: `-o, --output`: Write to a file, instead of STDOUT +- :nerd_face: `-q, --quiet`: Suppress the progress output - :nerd_face: `--platform=(amd64|arm64|...)`: Export content for a specific platform - :nerd_face: `--all-platforms`: Export content for all platforms diff --git a/pkg/api/types/image_types.go b/pkg/api/types/image_types.go index 2053d7dca06..d85e5f06665 100644 --- a/pkg/api/types/image_types.go +++ b/pkg/api/types/image_types.go @@ -279,6 +279,8 @@ type ImageSaveOptions struct { AllPlatforms bool // Export content for a specific platform Platform []string + // Quiet suppresses the progress output. + Quiet 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 a35a83a97f5..9c38bf79e4f 100644 --- a/pkg/cmd/image/save.go +++ b/pkg/cmd/image/save.go @@ -103,7 +103,11 @@ func Save(ctx context.Context, client *containerd.Client, images []string, optio w := nopWriteCloser{options.Stdout} - pf, done := transferutil.ProgressHandler(ctx, os.Stderr) + progressOutput := io.Writer(os.Stderr) + if options.Quiet { + progressOutput = io.Discard + } + pf, done := transferutil.ProgressHandler(ctx, progressOutput) defer done() return client.Transfer(ctx,