Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions cmd/nerdctl/image/image_convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,12 @@ func convertCommand() *cobra.Command {
cmd.Flags().Int64("soci-span-size", -1, "The size of SOCI spans")
// #endregion

// #region erofs flags
cmd.Flags().String("erofs", "", "Convert image layers to EROFS media type. Supported values: raw, zstd")
cmd.Flags().String("erofs-compressors", "", "Specify mkfs.erofs compressor options (e.g. 'lz4hc,12')")
cmd.Flags().String("erofs-mkfs-options", "", "Specify extra mkfs.erofs options (e.g. '-T0 --mkfs-time')")
// #endregion

// #region generic flags
cmd.Flags().Bool("uncompress", false, "Convert tar.gz layers to uncompressed tar layers")
cmd.Flags().Bool("oci", false, "Convert Docker media types to OCI media types")
Expand Down Expand Up @@ -248,6 +254,21 @@ func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) {
}
// #endregion

// #region erofs flags
erofs, err := cmd.Flags().GetString("erofs")
if err != nil {
return types.ImageConvertOptions{}, err
}
erofsCompressors, err := cmd.Flags().GetString("erofs-compressors")
if err != nil {
return types.ImageConvertOptions{}, err
}
erofsMkfsOptions, err := cmd.Flags().GetString("erofs-mkfs-options")
if err != nil {
return types.ImageConvertOptions{}, err
}
// #endregion

// #region generic flags
uncompress, err := cmd.Flags().GetBool("uncompress")
if err != nil {
Expand Down Expand Up @@ -323,6 +344,11 @@ func convertOptions(cmd *cobra.Command) (types.ImageConvertOptions, error) {
AllPlatforms: allPlatforms,
},
},
ErofsOptions: types.ErofsOptions{
Erofs: erofs,
ErofsCompressors: erofsCompressors,
ErofsMkfsOptions: erofsMkfsOptions,
},
ProgressOutput: progressOutput,
Stdout: cmd.OutOrStdout(),
}, nil
Expand Down
43 changes: 36 additions & 7 deletions cmd/nerdctl/image/image_convert_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"testing"
"time"

"github.com/containerd/nerdctl/mod/tigron/expect"
"github.com/containerd/nerdctl/mod/tigron/require"
"github.com/containerd/nerdctl/mod/tigron/test"

Expand Down Expand Up @@ -52,7 +53,7 @@ func TestImageConvert(t *testing.T) {
return helpers.Command("image", "convert", "--oci", "--estargz",
testutil.CommonImage, data.Identifier("converted-image"))
},
Expected: test.Expects(0, nil, nil),
Expected: test.Expects(expect.ExitCodeSuccess, nil, nil),
},
{
Description: "nydus",
Expand All @@ -66,7 +67,7 @@ func TestImageConvert(t *testing.T) {
return helpers.Command("image", "convert", "--oci", "--nydus",
testutil.CommonImage, data.Identifier("converted-image"))
},
Expected: test.Expects(0, nil, nil),
Expected: test.Expects(expect.ExitCodeSuccess, nil, nil),
},
{
Description: "zstd",
Expand All @@ -77,7 +78,7 @@ func TestImageConvert(t *testing.T) {
return helpers.Command("image", "convert", "--oci", "--zstd", "--zstd-compression-level", "3",
testutil.CommonImage, data.Identifier("converted-image"))
},
Expected: test.Expects(0, nil, nil),
Expected: test.Expects(expect.ExitCodeSuccess, nil, nil),
},
{
Description: "zstdchunked",
Expand All @@ -88,7 +89,35 @@ func TestImageConvert(t *testing.T) {
return helpers.Command("image", "convert", "--oci", "--zstdchunked", "--zstdchunked-compression-level", "3",
testutil.CommonImage, data.Identifier("converted-image"))
},
Expected: test.Expects(0, nil, nil),
Expected: test.Expects(expect.ExitCodeSuccess, nil, nil),
},
{
Description: "erofs raw",
Require: require.All(
require.Binary("mkfs.erofs"),
),
Cleanup: func(data test.Data, helpers test.Helpers) {
helpers.Anyhow("rmi", "-f", data.Identifier("converted-image"))
},
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
return helpers.Command("image", "convert", "--oci", "--erofs", "raw",
testutil.CommonImage, data.Identifier("converted-image"))
},
Expected: test.Expects(expect.ExitCodeSuccess, nil, nil),
},
{
Description: "erofs zstd",
Require: require.All(
require.Binary("mkfs.erofs"),
),
Cleanup: func(data test.Data, helpers test.Helpers) {
helpers.Anyhow("rmi", "-f", data.Identifier("converted-image"))
},
Command: func(data test.Data, helpers test.Helpers) test.TestableCommand {
return helpers.Command("image", "convert", "--oci", "--erofs", "zstd",
testutil.CommonImage, data.Identifier("converted-image"))
},
Expected: test.Expects(expect.ExitCodeSuccess, nil, nil),
},
{
Description: "soci",
Expand All @@ -107,7 +136,7 @@ func TestImageConvert(t *testing.T) {
"--soci-min-layer-size", "0",
testutil.CommonImage, data.Identifier("converted-image"))
},
Expected: test.Expects(0, nil, nil),
Expected: test.Expects(expect.ExitCodeSuccess, nil, nil),
},
{
Description: "soci with all-platforms",
Expand All @@ -126,7 +155,7 @@ func TestImageConvert(t *testing.T) {
"--soci-min-layer-size", "0",
testutil.CommonImage, data.Identifier("converted-image"))
},
Expected: test.Expects(0, nil, nil),
Expected: test.Expects(expect.ExitCodeSuccess, nil, nil),
},
},
}
Expand Down Expand Up @@ -188,7 +217,7 @@ func TestImageConvertNydusVerify(t *testing.T) {
cmd.WithTimeout(30 * time.Second)
return cmd
},
Expected: test.Expects(0, nil, nil),
Expected: test.Expects(expect.ExitCodeSuccess, nil, nil),
}

testCase.Run(t)
Expand Down
10 changes: 9 additions & 1 deletion cmd/nerdctl/image/image_inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@ import (

"github.com/spf13/cobra"

containerd "github.com/containerd/containerd/v2/client"
"github.com/containerd/platforms"

"github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
"github.com/containerd/nerdctl/v2/pkg/api/types"
"github.com/containerd/nerdctl/v2/pkg/clientutil"
"github.com/containerd/nerdctl/v2/pkg/cmd/image"
"github.com/containerd/nerdctl/v2/pkg/formatter"
"github.com/containerd/nerdctl/v2/pkg/platformutil"
)

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

client, ctx, cancel, err := clientutil.NewClientWithPlatform(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address, options.Platform)
var clientOpts []containerd.Opt
if options.Platform == "" {
clientOpts = append(clientOpts, containerd.WithDefaultPlatform(platformutil.IgnoreOSFeaturesMatcher(platforms.Default(), platformutil.ErofsOSFeature)))
}
client, ctx, cancel, err := clientutil.NewClientWithPlatform(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address, options.Platform, clientOpts...)
if err != nil {
return err
}
Expand Down
4 changes: 3 additions & 1 deletion docs/command-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1017,6 +1017,9 @@ Flags:
- `--overlaybd-fs-type=<TYPE>` : filesystem type for overlaybd (default: `ext4`)
- `--overlaybd-dbstr=<STRING>` : database config string for overlaybd
- `--overlaybd-vsize=<SIZE>` : virtual block device size in GB for overlaybd (default: 64)
- `--erofs=<MODE>` : convert image layers to EROFS media type. Supported values: `raw`, `zstd` (see [`./erofs.md`](./erofs.md))
- `--erofs-compressors=<COMPRESSORS>` : specify mkfs.erofs compressor options, e.g. `lz4hc,12`
- `--erofs-mkfs-options=<OPTIONS>` : specify extra mkfs.erofs options, e.g. `-T0 --mkfs-time`
- `--uncompress` : convert tar.gz layers to uncompressed tar layers
- `--oci` : convert Docker media types to OCI media types
- `--platform=<PLATFORM>` : convert content for a specific platform
Expand All @@ -1026,7 +1029,6 @@ Flags:
- `--soci-span-size` : Span size in bytes that soci index uses to segment layer data. Default is 4 MiB.
- `--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.


### :nerd_face: nerdctl image encrypt

Encrypt image layers. See [`./ocicrypt.md`](./ocicrypt.md).
Expand Down
53 changes: 53 additions & 0 deletions docs/erofs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# EROFS Image Conversion

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`.

## Prerequisites

- Install containerd with the `erofs` snapshotter and differ plugins enabled.
- Install `mkfs.erofs` for `nerdctl image convert --erofs`.

Check that containerd has loaded the EROFS plugins:

```console
ctr plugins ls | grep erofs
```

## Configure containerd transfer unpack

containerd 2.3+ provides an EROFS unpack configuration by default when the `erofs` snapshotter and differ plugins are available.

If `plugins."io.containerd.transfer.v1.local".unpack_config` is configured manually, add an EROFS entry to `/etc/containerd/config.toml` and restart containerd:

```toml
[[plugins."io.containerd.transfer.v1.local".unpack_config]]
platform = "linux(+erofs)/amd64"
snapshotter = "erofs"
differ = "erofs"
```

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.

## Convert an image

Convert an image to raw EROFS blobs:

```console
nerdctl image convert --erofs raw example.com/foo:latest example.com/foo:erofs
```

Convert an image to zstd-compressed EROFS blobs:

```console
nerdctl image convert --erofs zstd example.com/foo:latest example.com/foo:erofs-zstd
```

`--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.

## Pull and unpack with EROFS snapshotter

Push the converted image to a registry, then pull it with the `erofs` snapshotter:

```console
nerdctl image pull --snapshotter erofs example.com/foo:erofs
```
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ require (
require (
cyphar.com/go-pathrs v0.2.5 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/moby/moby/api v1.55.0 // indirect
github.com/moby/sys/capability v0.4.0 // indirect
go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect
Expand Down
11 changes: 11 additions & 0 deletions pkg/api/types/image_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ type ImageConvertOptions struct {
NydusOptions
OverlaybdOptions
SociConvertOptions
ErofsOptions
}

// EstargzOptions contains eStargz conversion options
Expand Down Expand Up @@ -152,6 +153,16 @@ type SociConvertOptions struct {
// #endregion
}

// ErofsOptions contains EROFS conversion options
type ErofsOptions struct {
// Erofs convert image layers to EROFS media type. Supported values: "raw" and "zstd"
Erofs string
// ErofsCompressors specifies mkfs compressor options, e.g. "lz4hc,12"
ErofsCompressors string
// ErofsMkfsOptions specifies extra options for mkfs.erofs, e.g. "-T0 --mkfs-time"
ErofsMkfsOptions string
}

// ImageCryptOptions specifies options for `nerdctl image encrypt` and `nerdctl image decrypt`.
type ImageCryptOptions struct {
Stdout io.Writer
Expand Down
37 changes: 35 additions & 2 deletions pkg/cmd/image/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/containerd/containerd/v2/core/content"
"github.com/containerd/containerd/v2/core/images"
"github.com/containerd/containerd/v2/core/images/converter"
erofsconvert "github.com/containerd/containerd/v2/core/images/converter/erofs"
"github.com/containerd/containerd/v2/core/images/converter/uncompress"
"github.com/containerd/log"
nydusconvert "github.com/containerd/nydus-snapshotter/pkg/converter"
Expand Down Expand Up @@ -92,8 +93,9 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa
overlaybd := options.Overlaybd
nydus := options.Nydus
soci := options.Soci
erofs := options.Erofs != ""
var finalize func(ctx context.Context, cs content.Store, ref string, desc *ocispec.Descriptor) (*images.Image, error)
if estargz || zstd || zstdchunked || overlaybd || nydus || soci {
if estargz || zstd || zstdchunked || overlaybd || nydus || soci || erofs {
convertCount := 0
if estargz {
convertCount++
Expand All @@ -113,12 +115,16 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa
if soci {
convertCount++
}
if erofs {
convertCount++
}

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

var convertFunc converter.ConvertFunc
var updateManifestFunc converter.UpdateManifestFunc
var convertType string
switch {
case estargz:
Expand Down Expand Up @@ -149,6 +155,12 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa
convertFunc = overlaybdconvert.IndexConvertFunc(obdOpts...)
convertOpts = append(convertOpts, converter.WithIndexConvertFunc(convertFunc))
convertType = "overlaybd"
case erofs:
convertFunc, updateManifestFunc, err = getErofsConverter(options)
if err != nil {
return err
}
convertType = "erofs"
case nydus:
nydusOpts, err := getNydusConvertOpts(options)
if err != nil {
Expand Down Expand Up @@ -188,6 +200,9 @@ func Convert(ctx context.Context, client *containerd.Client, srcRawRef, targetRa
if convertType != "overlaybd" {
convertOpts = append(convertOpts, converter.WithLayerConvertFunc(convertFunc))
}
if updateManifestFunc != nil {
convertOpts = append(convertOpts, converter.WithUpdateManifest(updateManifestFunc))
}
if !options.Oci {
if nydus || overlaybd {
log.G(ctx).Warnf("option --%s should be used in conjunction with --oci, forcibly enabling on oci mediatype for %s conversion", convertType, convertType)
Expand Down Expand Up @@ -369,6 +384,24 @@ func getZstdchunkedConverter(options types.ImageConvertOptions) (converter.Conve
return zstdchunkedconvert.LayerConvertFuncWithCompressionLevel(zstd.EncoderLevelFromZstd(options.ZstdChunkedCompressionLevel), esgzOpts...), nil
}

func getErofsConverter(options types.ImageConvertOptions) (converter.ConvertFunc, converter.UpdateManifestFunc, error) {
var convertOpts []erofsconvert.ConvertOpt
switch options.Erofs {
case "raw":
case "zstd":
convertOpts = append(convertOpts, erofsconvert.WithBlobCompression("zstd"))
default:
return nil, nil, fmt.Errorf("invalid value %q for --erofs, supported values are: raw, zstd", options.Erofs)
}
if options.ErofsCompressors != "" {
convertOpts = append(convertOpts, erofsconvert.WithCompressors(options.ErofsCompressors))
}
if options.ErofsMkfsOptions != "" {
convertOpts = append(convertOpts, erofsconvert.WithMkfsOptions(strings.Fields(options.ErofsMkfsOptions)))
}
return erofsconvert.LayerConvertFunc(convertOpts...), erofsconvert.UpdateManifestPlatform, nil
}

func getNydusConvertOpts(options types.ImageConvertOptions) (*nydusconvert.PackOption, error) {
workDir := options.NydusWorkDir
if workDir == "" {
Expand Down
8 changes: 6 additions & 2 deletions pkg/cmd/image/prune.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ import (
containerd "github.com/containerd/containerd/v2/client"
"github.com/containerd/containerd/v2/core/images"
"github.com/containerd/log"
"github.com/containerd/platforms"

"github.com/containerd/nerdctl/v2/pkg/api/types"
"github.com/containerd/nerdctl/v2/pkg/imgutil"
"github.com/containerd/nerdctl/v2/pkg/platformutil"
)

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

platformMatcher, err := platformutil.NewMatchComparer(false, nil)
if err != nil {
return err
}
delOpts := []images.DeleteOpt{images.SynchronousDelete()}
removedImages := make(map[string][]digest.Digest)
for _, image := range imagesToBeRemoved {
digests, err := image.RootFS(ctx, contentStore, platforms.DefaultStrict())
digests, err := image.RootFS(ctx, contentStore, platformMatcher)
if err != nil {
log.G(ctx).WithError(err).Warnf("failed to enumerate rootfs")
}
Expand Down
Loading
Loading