Skip to content

feat: support additional rootfs options besides ext4 #72

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
79 changes: 65 additions & 14 deletions builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ type builder struct {
splitBoot bool
bootSize uint64
bootFS BootFS
rootFS RootFS

loDevice string
bootPart string
Expand All @@ -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":
Expand Down Expand Up @@ -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,
}
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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))
}
Expand All @@ -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))
Expand All @@ -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
}
Expand Down Expand Up @@ -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)
}
}

Expand Down
1 change: 1 addition & 0 deletions cmd/d2vm/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
1 change: 1 addition & 0 deletions cmd/d2vm/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
15 changes: 14 additions & 1 deletion cmd/d2vm/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ var (
splitBoot bool
bootSize uint64
bootFS string
rootFS string = "ext4"
luksPassword string

keepCache bool
Expand Down Expand Up @@ -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")
}
Expand All @@ -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")
Expand Down
4 changes: 2 additions & 2 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
7 changes: 7 additions & 0 deletions convert_options.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type convertOptions struct {
splitBoot bool
bootSize uint64
bootFS BootFS
rootFS RootFS

luksPassword string

Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/content/reference/d2vm_build.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/content/reference/d2vm_convert.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down