Skip to content

Commit 694c405

Browse files
authored
Merge pull request #4061 from lengrongfu/master
[Feat] Add commit compression type support
2 parents 4d415b4 + 41e1a56 commit 694c405

File tree

7 files changed

+125
-22
lines changed

7 files changed

+125
-22
lines changed

cmd/nerdctl/container/container_commit.go

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package container
1818

1919
import (
20+
"errors"
21+
2022
"github.com/spf13/cobra"
2123

2224
"github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
@@ -40,6 +42,7 @@ func CommitCommand() *cobra.Command {
4042
cmd.Flags().StringP("message", "m", "", "Commit message")
4143
cmd.Flags().StringArrayP("change", "c", nil, "Apply Dockerfile instruction to the created image (supported directives: [CMD, ENTRYPOINT])")
4244
cmd.Flags().BoolP("pause", "p", true, "Pause container during commit")
45+
cmd.Flags().StringP("compression", "", "gzip", "commit compression algorithm (zstd or gzip)")
4346
return cmd
4447
}
4548

@@ -66,23 +69,29 @@ func commitOptions(cmd *cobra.Command) (types.ContainerCommitOptions, error) {
6669
return types.ContainerCommitOptions{}, err
6770
}
6871

72+
com, err := cmd.Flags().GetString("compression")
73+
if err != nil {
74+
return types.ContainerCommitOptions{}, err
75+
}
76+
if com != string(types.Zstd) && com != string(types.Gzip) {
77+
return types.ContainerCommitOptions{}, errors.New("--compression param only supports zstd or gzip")
78+
}
6979
return types.ContainerCommitOptions{
70-
Stdout: cmd.OutOrStdout(),
71-
GOptions: globalOptions,
72-
Author: author,
73-
Message: message,
74-
Pause: pause,
75-
Change: change,
80+
Stdout: cmd.OutOrStdout(),
81+
GOptions: globalOptions,
82+
Author: author,
83+
Message: message,
84+
Pause: pause,
85+
Change: change,
86+
Compression: types.CompressionType(com),
7687
}, nil
77-
7888
}
7989

8090
func commitAction(cmd *cobra.Command, args []string) error {
8191
options, err := commitOptions(cmd)
8292
if err != nil {
8393
return err
8494
}
85-
8695
client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address)
8796
if err != nil {
8897
return err

cmd/nerdctl/container/container_commit_test.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,14 @@ package container
1919
import (
2020
"testing"
2121

22+
"gotest.tools/v3/assert"
23+
2224
"github.com/containerd/nerdctl/mod/tigron/expect"
2325
"github.com/containerd/nerdctl/mod/tigron/require"
2426
"github.com/containerd/nerdctl/mod/tigron/test"
27+
"github.com/containerd/nerdctl/mod/tigron/tig"
2528

29+
"github.com/containerd/nerdctl/v2/pkg/inspecttypes/native"
2630
"github.com/containerd/nerdctl/v2/pkg/testutil"
2731
"github.com/containerd/nerdctl/v2/pkg/testutil/nerdtest"
2832
)
@@ -86,3 +90,52 @@ func TestCommit(t *testing.T) {
8690

8791
testCase.Run(t)
8892
}
93+
94+
func TestZstdCommit(t *testing.T) {
95+
testCase := nerdtest.Setup()
96+
testCase.Require = require.All(
97+
// FIXME: Docker does not support compression
98+
require.Not(nerdtest.Docker),
99+
nerdtest.ContainerdVersion("2.0.0"),
100+
nerdtest.CGroup,
101+
)
102+
testCase.Cleanup = func(data test.Data, helpers test.Helpers) {
103+
helpers.Anyhow("rm", "-f", data.Identifier())
104+
helpers.Anyhow("rmi", "-f", data.Identifier("image"))
105+
}
106+
testCase.Setup = func(data test.Data, helpers test.Helpers) {
107+
identifier := data.Identifier()
108+
helpers.Ensure("run", "-d", "--name", identifier, testutil.CommonImage, "sleep", nerdtest.Infinity)
109+
nerdtest.EnsureContainerStarted(helpers, identifier)
110+
helpers.Ensure("exec", identifier, "sh", "-euxc", `echo hello-test-commit > /foo`)
111+
helpers.Ensure("commit", identifier, data.Identifier("image"), "--compression=zstd")
112+
data.Labels().Set("image", data.Identifier("image"))
113+
}
114+
115+
testCase.SubTests = []*test.Case{
116+
{
117+
Description: "verify zstd has been used",
118+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
119+
return helpers.Command("image", "inspect", "--mode=native", data.Labels().Get("image"))
120+
},
121+
Expected: func(data test.Data, helpers test.Helpers) *test.Expected {
122+
return &test.Expected{
123+
ExitCode: 0,
124+
Output: expect.JSON([]native.Image{}, func(images []native.Image, s string, t tig.T) {
125+
assert.Equal(t, len(images), 1)
126+
assert.Equal(helpers.T(), images[0].Manifest.Layers[len(images[0].Manifest.Layers)-1].MediaType, "application/vnd.docker.image.rootfs.diff.tar.zstd")
127+
}),
128+
}
129+
},
130+
},
131+
{
132+
Description: "verify the image is working",
133+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
134+
return helpers.Command("run", "--rm", data.Labels().Get("image"), "sh", "-c", "--", "cat /foo")
135+
},
136+
Expected: test.Expects(0, nil, expect.Equals("hello-test-commit\n")),
137+
},
138+
}
139+
140+
testCase.Run(t)
141+
}

docs/command-reference.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,7 @@ Flags:
766766
- :whale: `-m, --message`: Commit message
767767
- :whale: `-c, --change`: Apply Dockerfile instruction to the created image (supported directives: [CMD, ENTRYPOINT])
768768
- :whale: `-p, --pause`: Pause container during commit (default: true)
769+
- :nerd_face: `--compression`: Commit compression algorithm (supported values: zstd or gzip) (default: gzip) (zstd is generally better for compression ratio but might not be as widely supported)
769770

770771
## Image management
771772

pkg/api/types/container_types.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,8 +385,17 @@ type ContainerCommitOptions struct {
385385
Change []string
386386
// Pause container during commit
387387
Pause bool
388+
// Compression is set commit compression algorithm
389+
Compression CompressionType
388390
}
389391

392+
type CompressionType string
393+
394+
const (
395+
Zstd CompressionType = "zstd"
396+
Gzip CompressionType = "gzip"
397+
)
398+
390399
// ContainerDiffOptions specifies options for `nerdctl (container) diff`.
391400
type ContainerDiffOptions struct {
392401
Stdout io.Writer

pkg/cmd/container/commit.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,12 @@ func Commit(ctx context.Context, client *containerd.Client, rawRef string, req s
4444
}
4545

4646
opts := &commit.Opts{
47-
Author: options.Author,
48-
Message: options.Message,
49-
Ref: parsedReference.String(),
50-
Pause: options.Pause,
51-
Changes: changes,
47+
Author: options.Author,
48+
Message: options.Message,
49+
Ref: parsedReference.String(),
50+
Pause: options.Pause,
51+
Changes: changes,
52+
Compression: options.Compression,
5253
}
5354

5455
walker := &containerwalker.ContainerWalker{

pkg/imgutil/commit/commit.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,12 @@ type Changes struct {
5757
}
5858

5959
type Opts struct {
60-
Author string
61-
Message string
62-
Ref string
63-
Pause bool
64-
Changes Changes
60+
Author string
61+
Message string
62+
Ref string
63+
Pause bool
64+
Changes Changes
65+
Compression types.CompressionType
6566
}
6667

6768
var (
@@ -176,7 +177,7 @@ func Commit(ctx context.Context, client *containerd.Client, container containerd
176177
// Sync filesystem to make sure that all the data writes in container could be persisted to disk.
177178
Sync()
178179

179-
diffLayerDesc, diffID, err := createDiff(ctx, id, sn, client.ContentStore(), differ)
180+
diffLayerDesc, diffID, err := createDiff(ctx, id, sn, client.ContentStore(), differ, opts.Compression)
180181
if err != nil {
181182
return emptyDigest, fmt.Errorf("failed to export layer: %w", err)
182183
}
@@ -356,8 +357,14 @@ func writeContentsForImage(ctx context.Context, snName string, baseImg container
356357
}
357358

358359
// createDiff creates a layer diff into containerd's content store.
359-
func createDiff(ctx context.Context, name string, sn snapshots.Snapshotter, cs content.Store, comparer diff.Comparer) (ocispec.Descriptor, digest.Digest, error) {
360-
newDesc, err := rootfs.CreateDiff(ctx, name, sn, comparer)
360+
func createDiff(ctx context.Context, name string, sn snapshots.Snapshotter, cs content.Store, comparer diff.Comparer, compression types.CompressionType) (ocispec.Descriptor, digest.Digest, error) {
361+
opts := make([]diff.Opt, 0)
362+
mediaType := images.MediaTypeDockerSchema2LayerGzip
363+
if compression == types.Zstd {
364+
opts = append(opts, diff.WithMediaType(ocispec.MediaTypeImageLayerZstd))
365+
mediaType = images.MediaTypeDockerSchema2LayerZstd
366+
}
367+
newDesc, err := rootfs.CreateDiff(ctx, name, sn, comparer, opts...)
361368
if err != nil {
362369
return ocispec.Descriptor{}, digest.Digest(""), err
363370
}
@@ -378,7 +385,7 @@ func createDiff(ctx context.Context, name string, sn snapshots.Snapshotter, cs c
378385
}
379386

380387
return ocispec.Descriptor{
381-
MediaType: images.MediaTypeDockerSchema2LayerGzip,
388+
MediaType: mediaType,
382389
Digest: newDesc.Digest,
383390
Size: info.Size,
384391
}, diffID, nil

pkg/testutil/nerdtest/requirements.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"os/exec"
2525
"strings"
2626

27+
"github.com/Masterminds/semver/v3"
2728
"gotest.tools/v3/assert"
2829

2930
"github.com/containerd/containerd/v2/defaults"
@@ -32,6 +33,7 @@ import (
3233

3334
"github.com/containerd/nerdctl/v2/pkg/buildkitutil"
3435
"github.com/containerd/nerdctl/v2/pkg/clientutil"
36+
"github.com/containerd/nerdctl/v2/pkg/infoutil"
3537
"github.com/containerd/nerdctl/v2/pkg/inspecttypes/dockercompat"
3638
"github.com/containerd/nerdctl/v2/pkg/rootlessutil"
3739
"github.com/containerd/nerdctl/v2/pkg/testutil"
@@ -416,3 +418,24 @@ var RemapIDs = &test.Requirement{
416418
return false, "snapshotter does not support ID remapping"
417419
},
418420
}
421+
422+
func ContainerdVersion(v string) *test.Requirement {
423+
return &test.Requirement{
424+
Check: func(data test.Data, helpers test.Helpers) (bool, string) {
425+
ctx := context.Background()
426+
namespace := defaultNamespace
427+
address := defaults.DefaultAddress
428+
client, ctx, cancel, err := clientutil.NewClient(ctx, namespace, address)
429+
if err != nil {
430+
return false, fmt.Sprintf("failed to create client: %v", err)
431+
}
432+
defer cancel()
433+
if sv, err := infoutil.ServerSemVer(ctx, client); err != nil {
434+
return false, err.Error()
435+
} else if sv.LessThan(semver.MustParse(v)) {
436+
return false, fmt.Sprintf("`nerdctl commit --compression expects containerd %s or later, got containerd %v", v, sv)
437+
}
438+
return true, ""
439+
},
440+
}
441+
}

0 commit comments

Comments
 (0)