Skip to content

Commit 168db2a

Browse files
committed
image: align erofs convert with containerd
Add raw and zstd EROFS conversion support with containerd's EROFS converter. Fixes: #4861 Signed-off-by: Chengyu Zhu <hudson@cyzhu.com>
1 parent 3364639 commit 168db2a

13 files changed

Lines changed: 276 additions & 20 deletions

File tree

cmd/nerdctl/image/image_convert.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ func convertCommand() *cobra.Command {
9797
cmd.Flags().Int64("soci-span-size", -1, "The size of SOCI spans")
9898
// #endregion
9999

100+
// #region erofs flags
101+
cmd.Flags().String("erofs", "", "Convert image layers to EROFS media type. Supported values: raw, zstd")
102+
cmd.Flags().String("erofs-compressors", "", "Specify mkfs.erofs compressor options (e.g. 'lz4hc,12')")
103+
cmd.Flags().String("erofs-mkfs-options", "", "Specify extra mkfs.erofs options (e.g. '-T0 --mkfs-time')")
104+
// #endregion
105+
100106
// #region generic flags
101107
cmd.Flags().Bool("uncompress", false, "Convert tar.gz layers to uncompressed tar layers")
102108
cmd.Flags().Bool("oci", false, "Convert Docker media types to OCI media types")
@@ -248,6 +254,21 @@ func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) {
248254
}
249255
// #endregion
250256

257+
// #region erofs flags
258+
erofs, err := cmd.Flags().GetString("erofs")
259+
if err != nil {
260+
return types.ImageConvertOptions{}, err
261+
}
262+
erofsCompressors, err := cmd.Flags().GetString("erofs-compressors")
263+
if err != nil {
264+
return types.ImageConvertOptions{}, err
265+
}
266+
erofsMkfsOptions, err := cmd.Flags().GetString("erofs-mkfs-options")
267+
if err != nil {
268+
return types.ImageConvertOptions{}, err
269+
}
270+
// #endregion
271+
251272
// #region generic flags
252273
uncompress, err := cmd.Flags().GetBool("uncompress")
253274
if err != nil {
@@ -323,6 +344,11 @@ func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) {
323344
AllPlatforms: allPlatforms,
324345
},
325346
},
347+
ErofsOptions: types.ErofsOptions{
348+
Erofs: erofs,
349+
ErofsCompressors: erofsCompressors,
350+
ErofsMkfsOptions: erofsMkfsOptions,
351+
},
326352
ProgressOutput: progressOutput,
327353
Stdout: cmd.OutOrStdout(),
328354
}, nil

cmd/nerdctl/image/image_convert_linux_test.go

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"testing"
2222
"time"
2323

24+
"github.com/containerd/nerdctl/mod/tigron/expect"
2425
"github.com/containerd/nerdctl/mod/tigron/require"
2526
"github.com/containerd/nerdctl/mod/tigron/test"
2627

@@ -52,7 +53,7 @@ func TestImageConvert(t *testing.T) {
5253
return helpers.Command("image", "convert", "--oci", "--estargz",
5354
testutil.CommonImage, data.Identifier("converted-image"))
5455
},
55-
Expected: test.Expects(0, nil, nil),
56+
Expected: test.Expects(expect.ExitCodeSuccess, nil, nil),
5657
},
5758
{
5859
Description: "nydus",
@@ -66,7 +67,7 @@ func TestImageConvert(t *testing.T) {
6667
return helpers.Command("image", "convert", "--oci", "--nydus",
6768
testutil.CommonImage, data.Identifier("converted-image"))
6869
},
69-
Expected: test.Expects(0, nil, nil),
70+
Expected: test.Expects(expect.ExitCodeSuccess, nil, nil),
7071
},
7172
{
7273
Description: "zstd",
@@ -77,7 +78,7 @@ func TestImageConvert(t *testing.T) {
7778
return helpers.Command("image", "convert", "--oci", "--zstd", "--zstd-compression-level", "3",
7879
testutil.CommonImage, data.Identifier("converted-image"))
7980
},
80-
Expected: test.Expects(0, nil, nil),
81+
Expected: test.Expects(expect.ExitCodeSuccess, nil, nil),
8182
},
8283
{
8384
Description: "zstdchunked",
@@ -88,7 +89,35 @@ func TestImageConvert(t *testing.T) {
8889
return helpers.Command("image", "convert", "--oci", "--zstdchunked", "--zstdchunked-compression-level", "3",
8990
testutil.CommonImage, data.Identifier("converted-image"))
9091
},
91-
Expected: test.Expects(0, nil, nil),
92+
Expected: test.Expects(expect.ExitCodeSuccess, nil, nil),
93+
},
94+
{
95+
Description: "erofs raw",
96+
Require: require.All(
97+
require.Binary("mkfs.erofs"),
98+
),
99+
Cleanup: func(data test.Data, helpers test.Helpers) {
100+
helpers.Anyhow("rmi", "-f", data.Identifier("converted-image"))
101+
},
102+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
103+
return helpers.Command("image", "convert", "--oci", "--erofs", "raw",
104+
testutil.CommonImage, data.Identifier("converted-image"))
105+
},
106+
Expected: test.Expects(expect.ExitCodeSuccess, nil, nil),
107+
},
108+
{
109+
Description: "erofs zstd",
110+
Require: require.All(
111+
require.Binary("mkfs.erofs"),
112+
),
113+
Cleanup: func(data test.Data, helpers test.Helpers) {
114+
helpers.Anyhow("rmi", "-f", data.Identifier("converted-image"))
115+
},
116+
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
117+
return helpers.Command("image", "convert", "--oci", "--erofs", "zstd",
118+
testutil.CommonImage, data.Identifier("converted-image"))
119+
},
120+
Expected: test.Expects(expect.ExitCodeSuccess, nil, nil),
92121
},
93122
{
94123
Description: "soci",
@@ -107,7 +136,7 @@ func TestImageConvert(t *testing.T) {
107136
"--soci-min-layer-size", "0",
108137
testutil.CommonImage, data.Identifier("converted-image"))
109138
},
110-
Expected: test.Expects(0, nil, nil),
139+
Expected: test.Expects(expect.ExitCodeSuccess, nil, nil),
111140
},
112141
{
113142
Description: "soci with all-platforms",
@@ -126,7 +155,7 @@ func TestImageConvert(t *testing.T) {
126155
"--soci-min-layer-size", "0",
127156
testutil.CommonImage, data.Identifier("converted-image"))
128157
},
129-
Expected: test.Expects(0, nil, nil),
158+
Expected: test.Expects(expect.ExitCodeSuccess, nil, nil),
130159
},
131160
},
132161
}
@@ -188,7 +217,7 @@ func TestImageConvertNydusVerify(t *testing.T) {
188217
cmd.WithTimeout(30 * time.Second)
189218
return cmd
190219
},
191-
Expected: test.Expects(0, nil, nil),
220+
Expected: test.Expects(expect.ExitCodeSuccess, nil, nil),
192221
}
193222

194223
testCase.Run(t)

cmd/nerdctl/image/image_inspect.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,16 @@ import (
2121

2222
"github.com/spf13/cobra"
2323

24+
containerd "github.com/containerd/containerd/v2/client"
25+
"github.com/containerd/platforms"
26+
2427
"github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
2528
"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
2629
"github.com/containerd/nerdctl/v2/pkg/api/types"
2730
"github.com/containerd/nerdctl/v2/pkg/clientutil"
2831
"github.com/containerd/nerdctl/v2/pkg/cmd/image"
2932
"github.com/containerd/nerdctl/v2/pkg/formatter"
33+
"github.com/containerd/nerdctl/v2/pkg/platformutil"
3034
)
3135

3236
func inspectCommand() *cobra.Command {
@@ -97,7 +101,11 @@ func imageInspectAction(cmd *cobra.Command, args []string) error {
97101
return fmt.Errorf("unknown mode %q", options.Mode)
98102
}
99103

100-
client, ctx, cancel, err := clientutil.NewClientWithPlatform(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address, options.Platform)
104+
var clientOpts []containerd.Opt
105+
if options.Platform == "" {
106+
clientOpts = append(clientOpts, containerd.WithDefaultPlatform(platformutil.IgnoreOSFeaturesMatcher(platforms.Default(), platformutil.ErofsOSFeature)))
107+
}
108+
client, ctx, cancel, err := clientutil.NewClientWithPlatform(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address, options.Platform, clientOpts...)
101109
if err != nil {
102110
return err
103111
}

docs/command-reference.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1029,6 +1029,9 @@ Flags:
10291029
- `--overlaybd-fs-type=<TYPE>` : filesystem type for overlaybd (default: `ext4`)
10301030
- `--overlaybd-dbstr=<STRING>` : database config string for overlaybd
10311031
- `--overlaybd-vsize=<SIZE>` : virtual block device size in GB for overlaybd (default: 64)
1032+
- `--erofs=<MODE>` : convert image layers to EROFS media type. Supported values: `raw`, `zstd` (see [`./erofs.md`](./erofs.md))
1033+
- `--erofs-compressors=<COMPRESSORS>` : specify mkfs.erofs compressor options, e.g. `lz4hc,12`
1034+
- `--erofs-mkfs-options=<OPTIONS>` : specify extra mkfs.erofs options, e.g. `-T0 --mkfs-time`
10321035
- `--uncompress` : convert tar.gz layers to uncompressed tar layers
10331036
- `--oci` : convert Docker media types to OCI media types
10341037
- `--platform=<PLATFORM>` : convert content for a specific platform
@@ -1038,7 +1041,6 @@ Flags:
10381041
- `--soci-span-size` : Span size in bytes that soci index uses to segment layer data. Default is 4 MiB.
10391042
- `--soci-min-layer-size`: Minimum layer size in bytes to build zTOC for. Smaller layers won't have zTOC and not lazy pulled. Default is 10 MiB.
10401043

1041-
10421044
### :nerd_face: nerdctl image encrypt
10431045

10441046
Encrypt image layers. See [`./ocicrypt.md`](./ocicrypt.md).

docs/erofs.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# EROFS Image Conversion
2+
3+
EROFS is a read-only filesystem supported by containerd's `erofs` snapshotter and differ. nerdctl can convert image layers to EROFS media types with `nerdctl image convert --erofs`.
4+
5+
## Prerequisites
6+
7+
- Install containerd with the `erofs` snapshotter and differ plugins enabled.
8+
- Install `mkfs.erofs` for `nerdctl image convert --erofs`.
9+
10+
Check that containerd has loaded the EROFS plugins:
11+
12+
```console
13+
ctr plugins ls | grep erofs
14+
```
15+
16+
## Configure containerd transfer unpack
17+
18+
containerd 2.3+ provides an EROFS unpack configuration by default when the `erofs` snapshotter and differ plugins are available.
19+
20+
If `plugins."io.containerd.transfer.v1.local".unpack_config` is configured manually, add an EROFS entry to `/etc/containerd/config.toml` and restart containerd:
21+
22+
```toml
23+
[[plugins."io.containerd.transfer.v1.local".unpack_config]]
24+
platform = "linux(+erofs)/amd64"
25+
snapshotter = "erofs"
26+
differ = "erofs"
27+
```
28+
29+
Replace `amd64` with the target architecture as needed. The `linux(+erofs)/ARCH` entry also allows the `erofs` snapshotter to unpack regular `linux/ARCH` tar/gzip images.
30+
31+
## Convert an image
32+
33+
Convert an image to raw EROFS blobs:
34+
35+
```console
36+
nerdctl image convert --erofs raw example.com/foo:latest example.com/foo:erofs
37+
```
38+
39+
Convert an image to zstd-compressed EROFS blobs:
40+
41+
```console
42+
nerdctl image convert --erofs zstd example.com/foo:latest example.com/foo:erofs-zstd
43+
```
44+
45+
`--erofs-compressors` passes compressor options to `mkfs.erofs`, and `--erofs-mkfs-options` passes extra `mkfs.erofs` options. See [`command-reference.md`](./command-reference.md) for flag details.
46+
47+
## Pull and unpack with EROFS snapshotter
48+
49+
Push the converted image to a registry, then pull it with the `erofs` snapshotter:
50+
51+
```console
52+
nerdctl image pull --snapshotter erofs example.com/foo:erofs
53+
```

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ require (
149149
require (
150150
cyphar.com/go-pathrs v0.2.5 // indirect
151151
github.com/cespare/xxhash/v2 v2.3.0 // indirect
152+
github.com/google/uuid v1.6.0 // indirect
152153
github.com/moby/moby/api v1.55.0 // indirect
153154
github.com/moby/sys/capability v0.4.0 // indirect
154155
go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect

pkg/api/types/image_types.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ type ImageConvertOptions struct {
7575
NydusOptions
7676
OverlaybdOptions
7777
SociConvertOptions
78+
ErofsOptions
7879
}
7980

8081
// EstargzOptions contains eStargz conversion options
@@ -152,6 +153,16 @@ type SociConvertOptions struct {
152153
// #endregion
153154
}
154155

156+
// ErofsOptions contains EROFS conversion options
157+
type ErofsOptions struct {
158+
// Erofs convert image layers to EROFS media type. Supported values: "raw" and "zstd"
159+
Erofs string
160+
// ErofsCompressors specifies mkfs compressor options, e.g. "lz4hc,12"
161+
ErofsCompressors string
162+
// ErofsMkfsOptions specifies extra options for mkfs.erofs, e.g. "-T0 --mkfs-time"
163+
ErofsMkfsOptions string
164+
}
165+
155166
// ImageCryptOptions specifies options for `nerdctl image encrypt` and `nerdctl image decrypt`.
156167
type ImageCryptOptions struct {
157168
Stdout io.Writer

pkg/cmd/image/convert.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"github.com/containerd/containerd/v2/core/content"
3434
"github.com/containerd/containerd/v2/core/images"
3535
"github.com/containerd/containerd/v2/core/images/converter"
36+
erofsconvert "github.com/containerd/containerd/v2/core/images/converter/erofs"
3637
"github.com/containerd/containerd/v2/core/images/converter/uncompress"
3738
"github.com/containerd/log"
3839
nydusconvert "github.com/containerd/nydus-snapshotter/pkg/converter"
@@ -92,8 +93,9 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa
9293
overlaybd := options.Overlaybd
9394
nydus := options.Nydus
9495
soci := options.Soci
96+
erofs := options.Erofs != ""
9597
var finalize func(ctx context.Context, cs content.Store, ref string, desc *ocispec.Descriptor) (*images.Image, error)
96-
if estargz || zstd || zstdchunked || overlaybd || nydus || soci {
98+
if estargz || zstd || zstdchunked || overlaybd || nydus || soci || erofs {
9799
convertCount := 0
98100
if estargz {
99101
convertCount++
@@ -113,12 +115,16 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa
113115
if soci {
114116
convertCount++
115117
}
118+
if erofs {
119+
convertCount++
120+
}
116121

117122
if convertCount > 1 {
118-
return errors.New("options --estargz, --zstdchunked, --overlaybd, --nydus and --soci lead to conflict, only one of them can be used")
123+
return errors.New("options --estargz, --zstdchunked, --overlaybd, --nydus, --soci and --erofs lead to conflict, only one of them can be used")
119124
}
120125

121126
var convertFunc converter.ConvertFunc
127+
var updateManifestFunc converter.UpdateManifestFunc
122128
var convertType string
123129
switch {
124130
case estargz:
@@ -149,6 +155,12 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa
149155
convertFunc = overlaybdconvert.IndexConvertFunc(obdOpts...)
150156
convertOpts = append(convertOpts, converter.WithIndexConvertFunc(convertFunc))
151157
convertType = "overlaybd"
158+
case erofs:
159+
convertFunc, updateManifestFunc, err = getErofsConverter(options)
160+
if err != nil {
161+
return err
162+
}
163+
convertType = "erofs"
152164
case nydus:
153165
nydusOpts, err := getNydusConvertOpts(options)
154166
if err != nil {
@@ -188,6 +200,9 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa
188200
if convertType != "overlaybd" {
189201
convertOpts = append(convertOpts, converter.WithLayerConvertFunc(convertFunc))
190202
}
203+
if updateManifestFunc != nil {
204+
convertOpts = append(convertOpts, converter.WithUpdateManifest(updateManifestFunc))
205+
}
191206
if !options.Oci {
192207
if nydus || overlaybd {
193208
log.G(ctx).Warnf("option --%s should be used in conjunction with --oci, forcibly enabling on oci mediatype for %s conversion", convertType, convertType)
@@ -369,6 +384,24 @@ func getZstdchunkedConverter(options types.ImageConvertOptions) (converter.Conve
369384
return zstdchunkedconvert.LayerConvertFuncWithCompressionLevel(zstd.EncoderLevelFromZstd(options.ZstdChunkedCompressionLevel), esgzOpts...), nil
370385
}
371386

387+
func getErofsConverter(options types.ImageConvertOptions) (converter.ConvertFunc, converter.UpdateManifestFunc, error) {
388+
var convertOpts []erofsconvert.ConvertOpt
389+
switch options.Erofs {
390+
case "raw":
391+
case "zstd":
392+
convertOpts = append(convertOpts, erofsconvert.WithBlobCompression("zstd"))
393+
default:
394+
return nil, nil, fmt.Errorf("invalid value %q for --erofs, supported values are: raw, zstd", options.Erofs)
395+
}
396+
if options.ErofsCompressors != "" {
397+
convertOpts = append(convertOpts, erofsconvert.WithCompressors(options.ErofsCompressors))
398+
}
399+
if options.ErofsMkfsOptions != "" {
400+
convertOpts = append(convertOpts, erofsconvert.WithMkfsOptions(strings.Fields(options.ErofsMkfsOptions)))
401+
}
402+
return erofsconvert.LayerConvertFunc(convertOpts...), erofsconvert.UpdateManifestPlatform, nil
403+
}
404+
372405
func getNydusConvertOpts(options types.ImageConvertOptions) (*nydusconvert.PackOption, error) {
373406
workDir := options.NydusWorkDir
374407
if workDir == "" {

pkg/cmd/image/prune.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ import (
2525
containerd "github.com/containerd/containerd/v2/client"
2626
"github.com/containerd/containerd/v2/core/images"
2727
"github.com/containerd/log"
28-
"github.com/containerd/platforms"
2928

3029
"github.com/containerd/nerdctl/v2/pkg/api/types"
3130
"github.com/containerd/nerdctl/v2/pkg/imgutil"
31+
"github.com/containerd/nerdctl/v2/pkg/platformutil"
3232
)
3333

3434
// Prune will remove all dangling images. If all is specified, will also remove all images not referenced by any container.
@@ -68,10 +68,14 @@ func Prune(ctx context.Context, client *containerd.Client, options types.ImagePr
6868
return err
6969
}
7070

71+
platformMatcher, err := platformutil.NewMatchComparer(false, nil)
72+
if err != nil {
73+
return err
74+
}
7175
delOpts := []images.DeleteOpt{images.SynchronousDelete()}
7276
removedImages := make(map[string][]digest.Digest)
7377
for _, image := range imagesToBeRemoved {
74-
digests, err := image.RootFS(ctx, contentStore, platforms.DefaultStrict())
78+
digests, err := image.RootFS(ctx, contentStore, platformMatcher)
7579
if err != nil {
7680
log.G(ctx).WithError(err).Warnf("failed to enumerate rootfs")
7781
}

0 commit comments

Comments
 (0)