diff --git a/builder.go b/builder.go index 04d89aa..f430a03 100644 --- a/builder.go +++ b/builder.go @@ -68,6 +68,7 @@ type builder struct { splitBoot bool bootSize uint64 bootFS BootFS + rootFS RootFS loDevice string bootPart string @@ -85,7 +86,7 @@ type builder struct { arch string } -func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64, osRelease OSRelease, format string, cmdLineExtra string, splitBoot bool, bootFS BootFS, bootSize uint64, luksPassword string, bootLoader string, platform string) (Builder, error) { +func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64, osRelease OSRelease, format string, cmdLineExtra string, splitBoot bool, bootFS BootFS, bootSize uint64, rootFS RootFS, luksPassword string, bootLoader string, platform string) (Builder, error) { var arch string switch platform { case "linux/amd64": @@ -193,6 +194,7 @@ func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64, splitBoot: splitBoot, bootSize: bootSize, bootFS: bootFS, + rootFS: rootFS, luksPassword: luksPassword, arch: arch, } @@ -211,7 +213,7 @@ func (b *builder) Build(ctx context.Context) (err error) { return } logrus.WithError(err).Error("Build failed") - if err := b.unmountImg(context.Background()); err != nil { + if err := b.unmountImg(context.Background(), false); err != nil { logrus.WithError(err).Error("failed to unmount") } if err := b.cleanUp(context.Background()); err != nil { @@ -236,7 +238,7 @@ func (b *builder) Build(ctx context.Context) (err error) { if err = b.installBootloader(ctx); err != nil { return err } - if err = b.unmountImg(ctx); err != nil { + if err = b.unmountImg(ctx, true); err != nil { return err } if err = b.convert2Img(ctx); err != nil { @@ -279,6 +281,18 @@ func (b *builder) makeImg(ctx context.Context) error { return nil } +func makeRootFS(ctx context.Context, rootFS RootFS, bootPart string) error { + switch rootFS { + default: + case RootFSExt4: + return exec.Run(ctx, "mkfs.ext4", bootPart) + case RootFSBtrfs: + return exec.Run(ctx, "mkfs.btrfs", bootPart) + } + + return nil +} + func (b *builder) mountImg(ctx context.Context) error { logrus.Infof("mounting raw image") o, _, err := exec.RunOut(ctx, "losetup", "--show", "-f", b.diskRaw) @@ -314,16 +328,16 @@ func (b *builder) mountImg(ctx context.Context) error { b.cryptPart = b.rootPart b.rootPart = "/dev/mapper/root" b.mappedCryptRoot = filepath.Join("/dev/mapper", b.cryptRoot) - logrus.Infof("creating raw image file system") - if err := exec.Run(ctx, "mkfs.ext4", b.mappedCryptRoot); err != nil { + logrus.Infof("creating raw image file system (%s)", b.rootFS) + if err := makeRootFS(ctx, b.rootFS, b.mappedCryptRoot); err != nil { return err } if err := exec.Run(ctx, "mount", b.mappedCryptRoot, b.mntPoint); err != nil { return err } } else { - logrus.Infof("creating raw image file system") - if err := exec.Run(ctx, "mkfs.ext4", b.rootPart); err != nil { + logrus.Infof("creating raw image file system (%s)", b.rootFS) + if err := makeRootFS(ctx, b.rootFS, b.rootPart); err != nil { return err } if err := exec.Run(ctx, "mount", b.rootPart, b.mntPoint); err != nil { @@ -350,13 +364,23 @@ func (b *builder) mountImg(ctx context.Context) error { return nil } -func (b *builder) unmountImg(ctx context.Context) error { +func (b *builder) unmountImg(ctx context.Context, success bool) error { logrus.Infof("unmounting raw image") var merr error if b.splitBoot { merr = multierr.Append(merr, exec.Run(ctx, "umount", filepath.Join(b.mntPoint, "boot"))) } + + // Try to cleanup the filesystem empty space before unmounting (success only) + if success { + logrus.Infof("triming root filesystem") + if err := exec.Run(ctx, "fstrim", b.mntPoint); err != nil { + logrus.Errorf("ERROR: failed to trim filesystem: %s", err) + } + } + merr = multierr.Append(merr, exec.Run(ctx, "umount", b.mntPoint)) + if b.isLuksEnabled() { merr = multierr.Append(merr, exec.Run(ctx, "cryptsetup", "close", b.mappedCryptRoot)) } @@ -383,6 +407,31 @@ func diskUUID(ctx context.Context, disk string) (string, error) { return strings.TrimSuffix(o, "\n"), nil } +func (b *builder) fstabEntry() (string, error) { + rootOpts := "defaults" + var sb strings.Builder + + switch b.rootFS { + case RootFSExt4: + rootOpts += ",errors=remount-ro" + default: + } + + if _, err := fmt.Fprintf(&sb, "UUID=%s / %s %s 0 1\n", b.rootUUID, b.rootFS, rootOpts); err != nil { + return "", err + } + + if !b.splitBoot { + return sb.String(), nil + } + + if _, err := fmt.Fprintf(&sb, "UUID=%s /boot %s errors=remount-ro 0 2\n", b.bootUUID, b.bootFS.linux()); err != nil { + return "", err + } + + return sb.String(), nil +} + func (b *builder) setupRootFS(ctx context.Context) (err error) { logrus.Infof("setting up rootfs") b.rootUUID, err = diskUUID(ctx, ifElse(b.isLuksEnabled(), b.mappedCryptRoot, b.rootPart)) @@ -401,11 +450,13 @@ func (b *builder) setupRootFS(ctx context.Context) (err error) { return err } } - fstab = fmt.Sprintf("UUID=%s / ext4 errors=remount-ro 0 1\nUUID=%s /boot %s errors=remount-ro 0 2\n", b.rootUUID, b.bootUUID, b.bootFS.linux()) } else { b.bootUUID = b.rootUUID - fstab = fmt.Sprintf("UUID=%s / ext4 errors=remount-ro 0 1\n", b.bootUUID) } + if fstab, err = b.fstabEntry(); err != nil { + return err + } + if err := b.chWriteFile("/etc/fstab", fstab, perm); err != nil { return err } @@ -447,18 +498,18 @@ func (b *builder) setupRootFS(ctx context.Context) (err error) { func (b *builder) cmdline(_ context.Context) string { if !b.isLuksEnabled() { - return b.config.Cmdline(RootUUID(b.rootUUID), b.cmdLineExtra) + return b.config.Cmdline(RootUUID(b.rootUUID), b.rootFS, b.cmdLineExtra) } switch b.osRelease.ID { case ReleaseAlpine: - return b.config.Cmdline(RootUUID(b.rootUUID), "root=/dev/mapper/root", "cryptdm=root", "cryptroot=UUID="+b.cryptUUID, b.cmdLineExtra) + return b.config.Cmdline(RootUUID(b.rootUUID), b.rootFS, "root=/dev/mapper/root", "cryptdm=root", "cryptroot=UUID="+b.cryptUUID, b.cmdLineExtra) case ReleaseCentOS: - return b.config.Cmdline(RootUUID(b.rootUUID), "rd.luks.name=UUID="+b.rootUUID+" rd.luks.uuid="+b.cryptUUID+" rd.luks.crypttab=0", b.cmdLineExtra) + return b.config.Cmdline(RootUUID(b.rootUUID), b.rootFS, "rd.luks.name=UUID="+b.rootUUID+" rd.luks.uuid="+b.cryptUUID+" rd.luks.crypttab=0", b.cmdLineExtra) default: // for some versions of debian, the cryptopts parameter MUST contain all the following: target,source,key,opts... // see https://salsa.debian.org/cryptsetup-team/cryptsetup/-/blob/debian/buster/debian/functions // and https://cryptsetup-team.pages.debian.net/cryptsetup/README.initramfs.html - return b.config.Cmdline(nil, "root=/dev/mapper/root", "cryptopts=target=root,source=UUID="+b.cryptUUID+",key=none,luks", b.cmdLineExtra) + return b.config.Cmdline(nil, b.rootFS, "root=/dev/mapper/root", "cryptopts=target=root,source=UUID="+b.cryptUUID+",key=none,luks", b.cmdLineExtra) } } diff --git a/cmd/d2vm/build.go b/cmd/d2vm/build.go index 3d9f97e..d8ed744 100644 --- a/cmd/d2vm/build.go +++ b/cmd/d2vm/build.go @@ -106,6 +106,7 @@ var ( d2vm.WithSplitBoot(splitBoot), d2vm.WithBootSize(bootSize), d2vm.WithBootFS(d2vm.BootFS(bootFS)), + d2vm.WithRootFS(d2vm.RootFS(rootFS)), d2vm.WithLuksPassword(luksPassword), d2vm.WithKeepCache(keepCache), d2vm.WithPlatform(platform), diff --git a/cmd/d2vm/convert.go b/cmd/d2vm/convert.go index 6aea732..c02e51b 100644 --- a/cmd/d2vm/convert.go +++ b/cmd/d2vm/convert.go @@ -87,6 +87,7 @@ var ( d2vm.WithSplitBoot(splitBoot), d2vm.WithBootSize(bootSize), d2vm.WithBootFS(d2vm.BootFS(bootFS)), + d2vm.WithRootFS(d2vm.RootFS(rootFS)), d2vm.WithLuksPassword(luksPassword), d2vm.WithKeepCache(keepCache), d2vm.WithPlatform(platform), diff --git a/cmd/d2vm/flags.go b/cmd/d2vm/flags.go index b97abbd..3a33c2f 100644 --- a/cmd/d2vm/flags.go +++ b/cmd/d2vm/flags.go @@ -40,6 +40,7 @@ var ( splitBoot bool bootSize uint64 bootFS string + rootFS string = "ext4" luksPassword string keepCache bool @@ -86,6 +87,17 @@ func validateFlags() error { logrus.Warnf("grub-efi bootloader is set: enabling fat32 boot filesystem") bootFS = "fat32" } + + switch rootFS { + case "btrfs": + rootFS = "btrfs" + case "ext4": + case "": + rootFS = "ext4" + default: + return fmt.Errorf("invalid root filesystem: %s", rootFS) + } + if push && tag == "" { return fmt.Errorf("tag is required when pushing container disk image") } @@ -110,8 +122,9 @@ func buildFlags() *pflag.FlagSet { flags.BoolVar(&push, "push", false, "Push the container disk image to the registry") flags.BoolVar(&splitBoot, "split-boot", false, "Split the boot partition from the root partition") flags.Uint64Var(&bootSize, "boot-size", 100, "Size of the boot partition in MB") - flags.StringVar(&bootFS, "boot-fs", "", "Filesystem to use for the boot partition, ext4 or fat32") + flags.StringVar(&bootFS, "boot-fs", "ext4", "Filesystem to use for the boot partition, ext4 or fat32") flags.StringVar(&bootloader, "bootloader", "", "Bootloader to use: syslinux, grub, grub-bios, grub-efi, defaults to syslinux on amd64 and grub-efi on arm64") + flags.StringVar(&rootFS, "root-fs", rootFS, "Filesystem to use for the root partition [ext4, btrfs], (default: ext4)") flags.StringVar(&luksPassword, "luks-password", "", "Password to use for the LUKS encrypted root partition. If not set, the root partition will not be encrypted") flags.BoolVar(&keepCache, "keep-cache", false, "Keep the images after the build") flags.StringVar(&platform, "platform", d2vm.Arch, "Platform to use for the container disk image, linux/arm64 and linux/arm64 are supported") diff --git a/config.go b/config.go index 86b4596..4a90962 100644 --- a/config.go +++ b/config.go @@ -59,12 +59,12 @@ type Config struct { Initrd string } -func (c Config) Cmdline(root Root, args ...string) string { +func (c Config) Cmdline(root Root, rootFS RootFS, args ...string) string { var r string if root != nil { r = fmt.Sprintf("root=%s", root.String()) } - return fmt.Sprintf("ro initrd=%s %s net.ifnames=0 rootfstype=ext4 console=tty0 console=ttyS0,115200n8 %s", c.Initrd, r, strings.Join(args, " ")) + return fmt.Sprintf("ro initrd=%s %s net.ifnames=0 rootfstype=%s console=tty0 console=ttyS0,115200n8 %s", c.Initrd, r, rootFS, strings.Join(args, " ")) } func (r OSRelease) Config() (Config, error) { diff --git a/convert.go b/convert.go index 769de35..433785d 100644 --- a/convert.go +++ b/convert.go @@ -88,7 +88,7 @@ func Convert(ctx context.Context, img string, opts ...ConvertOption) error { if format == "" { format = "raw" } - b, err := NewBuilder(ctx, tmpPath, imgUUID, "", o.size, r, format, o.cmdLineExtra, o.splitBoot, o.bootFS, o.bootSize, o.luksPassword, o.bootLoader, o.platform) + b, err := NewBuilder(ctx, tmpPath, imgUUID, "", o.size, r, format, o.cmdLineExtra, o.splitBoot, o.bootFS, o.bootSize, o.rootFS, o.luksPassword, o.bootLoader, o.platform) if err != nil { return err } diff --git a/convert_options.go b/convert_options.go index 47efd13..73a9694 100644 --- a/convert_options.go +++ b/convert_options.go @@ -28,6 +28,7 @@ type convertOptions struct { splitBoot bool bootSize uint64 bootFS BootFS + rootFS RootFS luksPassword string @@ -104,6 +105,12 @@ func WithBootFS(bootFS BootFS) ConvertOption { } } +func WithRootFS(rootFS RootFS) ConvertOption { + return func(o *convertOptions) { + o.rootFS = rootFS + } +} + func WithLuksPassword(password string) ConvertOption { return func(o *convertOptions) { o.luksPassword = password diff --git a/docs/content/reference/d2vm_build.md b/docs/content/reference/d2vm_build.md index 110eab7..6527921 100644 --- a/docs/content/reference/d2vm_build.md +++ b/docs/content/reference/d2vm_build.md @@ -12,6 +12,7 @@ d2vm build [context directory] [flags] --append-to-cmdline string Extra kernel cmdline arguments to append to the generated one --boot-fs string Filesystem to use for the boot partition, ext4 or fat32 --boot-size uint Size of the boot partition in MB (default 100) + --root-fs string Filesystem to use for root partition [ext4, btrfs] (default: ext4) --bootloader string Bootloader to use: syslinux, grub, grub-bios, grub-efi, defaults to syslinux on amd64 and grub-efi on arm64 --build-arg stringArray Set build-time variables -f, --file string Name of the Dockerfile diff --git a/docs/content/reference/d2vm_convert.md b/docs/content/reference/d2vm_convert.md index e143acb..38e8a97 100644 --- a/docs/content/reference/d2vm_convert.md +++ b/docs/content/reference/d2vm_convert.md @@ -11,6 +11,7 @@ d2vm convert [docker image] [flags] ``` --append-to-cmdline string Extra kernel cmdline arguments to append to the generated one --boot-fs string Filesystem to use for the boot partition, ext4 or fat32 + --root-fs string Filesystem to use for root partition [ext4, btrfs] (default: ext4) --boot-size uint Size of the boot partition in MB (default 100) --bootloader string Bootloader to use: syslinux, grub, grub-bios, grub-efi, defaults to syslinux on amd64 and grub-efi on arm64 --force Override output qcow2 image diff --git a/fs.go b/fs.go index 7cb3027..12544d2 100644 --- a/fs.go +++ b/fs.go @@ -19,12 +19,18 @@ import ( ) type BootFS string +type RootFS string const ( BootFSExt4 BootFS = "ext4" BootFSFat32 BootFS = "fat32" ) +const ( + RootFSExt4 RootFS = "ext4" + RootFSBtrfs RootFS = "btrfs" +) + func (f BootFS) String() string { return string(f) }