From bb9657d13b1c16a0ca6c0f714ff83e46546f86d7 Mon Sep 17 00:00:00 2001 From: Elias Abacioglu <1148206+Raboo@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:46:14 +0200 Subject: [PATCH 01/20] feat: add RHEL 10 GRUB support with grub2 commands and EFI binary handling - Add isRhelFamily() helper to centralize RHEL-family distro detection - Use grub2-install/grub2-mkconfig for all RHEL-family distros (CentOS, Rocky, AlmaLinux, RHEL) - Enable grub and grub-efi bootloaders for Rocky, AlmaLinux, and CentOS - Copy distro-specific EFI binary to removable boot path for RHEL-family EFI support - Consolidate SupportsLUKS() switch cases for RHEL-family distros --- grub.go | 3 --- grub_common.go | 6 +++++- grub_efi.go | 39 ++++++++++++++++++++++++++++++++++++--- os_release.go | 8 ++------ 4 files changed, 43 insertions(+), 13 deletions(-) diff --git a/grub.go b/grub.go index e853f4a..189e411 100644 --- a/grub.go +++ b/grub.go @@ -61,9 +61,6 @@ func (g grubProvider) New(c Config, r OSRelease, arch string) (Bootloader, error if arch != "x86_64" { return nil, fmt.Errorf("grub is only supported for amd64") } - if r.ID == ReleaseCentOS || r.ID == ReleaseRocky || r.ID == ReleaseAlmaLinux { - return nil, fmt.Errorf("grub (efi) is not supported for CentOS / Rocky / AlmaLinux, use grub-bios instead") - } return grub{grubCommon: newGrubCommon(c, r)}, nil } diff --git a/grub_common.go b/grub_common.go index 6e41095..a30fbab 100644 --- a/grub_common.go +++ b/grub_common.go @@ -42,9 +42,13 @@ type grubCommon struct { dev string } +func isRhelFamily(r Release) bool { + return r == ReleaseCentOS || r == ReleaseRocky || r == ReleaseAlmaLinux || r == ReleaseRHEL +} + func newGrubCommon(c Config, r OSRelease) *grubCommon { name := "grub" - if r.ID == "centos" { + if isRhelFamily(r.ID) { name = "grub2" } return &grubCommon{ diff --git a/grub_efi.go b/grub_efi.go index 4c3a83f..c9cb494 100644 --- a/grub_efi.go +++ b/grub_efi.go @@ -17,8 +17,12 @@ package d2vm import ( "context" "fmt" + "os" + "path/filepath" "github.com/sirupsen/logrus" + + "go.linka.cloud/d2vm/pkg/exec" ) type grubEFI struct { @@ -45,20 +49,49 @@ func (g grubEFI) Setup(ctx context.Context, dev, root string, cmdline string) er if err := g.install(ctx, "--target="+g.arch+"-efi", "--efi-directory=/boot", "--no-nvram", "--removable", "--no-floppy"); err != nil { return err } + if isRhelFamily(g.r.ID) { + if err := g.copyEfiBinary(ctx, root); err != nil { + return err + } + } if err := g.mkconfig(ctx); err != nil { return err } return nil } +func (g grubEFI) copyEfiBinary(ctx context.Context, root string) error { + efiDir := filepath.Join(root, "boot", "efi", "EFI") + distroDirs := []string{"rocky", "almalinux", "centos", "rhel"} + var srcPath string + for _, d := range distroDirs { + p := filepath.Join(efiDir, d, "grub"+g.arch+".efi") + if _, err := os.Stat(p); err == nil { + srcPath = p + break + } + } + if srcPath == "" { + logrus.Warnf("could not find distro-specific EFI binary, skipping removable boot copy") + return nil + } + dstDir := filepath.Join(efiDir, "BOOT") + dstPath := filepath.Join(dstDir, "BOOT"+g.arch+".EFI") + if err := os.MkdirAll(dstDir, os.ModePerm); err != nil { + return fmt.Errorf("failed to create EFI boot directory: %w", err) + } + if err := exec.Run(ctx, "cp", srcPath, dstPath); err != nil { + return fmt.Errorf("failed to copy EFI binary to removable boot path: %w", err) + } + logrus.Infof("copied EFI binary to %s", dstPath) + return nil +} + type grubEFIProvider struct { config Config } func (g grubEFIProvider) New(c Config, r OSRelease, arch string) (Bootloader, error) { - if r.ID == ReleaseCentOS || r.ID == ReleaseRocky || r.ID == ReleaseAlmaLinux { - return nil, fmt.Errorf("grub-efi is not supported for CentOS, use grub-bios instead") - } return grubEFI{grubCommon: newGrubCommon(c, r), arch: arch}, nil } diff --git a/os_release.go b/os_release.go index a0f7197..ad7f586 100644 --- a/os_release.go +++ b/os_release.go @@ -83,14 +83,10 @@ func (r OSRelease) SupportsLUKS() bool { case ReleaseKali: // TODO: check version return true - case ReleaseCentOS: - return true - case ReleaseRocky: - return true - case ReleaseAlmaLinux: - return true case ReleaseAlpine: return true + case ReleaseCentOS, ReleaseRocky, ReleaseAlmaLinux: + return true case ReleaseRHEL: return false default: From ace561f7dced2ccf48f24a3c000f730e382b2c7f Mon Sep 17 00:00:00 2001 From: Elias Abacioglu <1148206+Raboo@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:40:37 +0200 Subject: [PATCH 02/20] fix: add --force flag to grub2-install for EFI platforms in chroot grub2-install on RHEL-family EFI platforms requires --force when running in a chroot/offline environment without EFI variables. This is standard for VM image building scenarios. --- grub.go | 2 +- grub_efi.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/grub.go b/grub.go index 189e411..76018fe 100644 --- a/grub.go +++ b/grub.go @@ -41,7 +41,7 @@ func (g grub) Setup(ctx context.Context, dev, root string, cmdline string) error return err } defer clean() - if err := g.install(ctx, "--target=x86_64-efi", "--efi-directory=/boot", "--no-nvram", "--removable", "--no-floppy"); err != nil { + if err := g.install(ctx, "--target=x86_64-efi", "--efi-directory=/boot", "--no-nvram", "--removable", "--no-floppy", "--force"); err != nil { return err } if err := g.install(ctx, "--target=i386-pc", "--boot-directory=/boot", dev); err != nil { diff --git a/grub_efi.go b/grub_efi.go index c9cb494..009d709 100644 --- a/grub_efi.go +++ b/grub_efi.go @@ -46,7 +46,7 @@ func (g grubEFI) Setup(ctx context.Context, dev, root string, cmdline string) er return err } defer clean() - if err := g.install(ctx, "--target="+g.arch+"-efi", "--efi-directory=/boot", "--no-nvram", "--removable", "--no-floppy"); err != nil { + if err := g.install(ctx, "--target="+g.arch+"-efi", "--efi-directory=/boot", "--no-nvram", "--removable", "--no-floppy", "--force"); err != nil { return err } if isRhelFamily(g.r.ID) { From 782599306be16acb99019a277db6d1e9027797f9 Mon Sep 17 00:00:00 2001 From: Elias Abacioglu <1148206+Raboo@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:57:19 +0200 Subject: [PATCH 03/20] feat: add Rocky Linux template and example - Add templates/rocky.Dockerfile for Rocky/AlmaLinux image builds - Add examples/rocky.Dockerfile following existing example patterns - Route Rocky and AlmaLinux to use the new rocky template - Keep CentOS using its own centos.Dockerfile template --- dockerfile.go | 12 +++++++++- examples/rocky.Dockerfile | 10 +++++++++ templates/rocky.Dockerfile | 45 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 examples/rocky.Dockerfile create mode 100644 templates/rocky.Dockerfile diff --git a/dockerfile.go b/dockerfile.go index 4f91319..cb9afc5 100644 --- a/dockerfile.go +++ b/dockerfile.go @@ -36,11 +36,15 @@ var alpineDockerfile string //go:embed templates/centos.Dockerfile var centOSDockerfile string +//go:embed templates/rocky.Dockerfile +var rockyDockerfile string + var ( ubuntuDockerfileTemplate = template.Must(template.New("ubuntu.Dockerfile").Funcs(tplFuncs).Parse(ubuntuDockerfile)) debianDockerfileTemplate = template.Must(template.New("debian.Dockerfile").Funcs(tplFuncs).Parse(debianDockerfile)) alpineDockerfileTemplate = template.Must(template.New("alpine.Dockerfile").Funcs(tplFuncs).Parse(alpineDockerfile)) centOSDockerfileTemplate = template.Must(template.New("centos.Dockerfile").Funcs(tplFuncs).Parse(centOSDockerfile)) + rockyDockerfileTemplate = template.Must(template.New("rocky.Dockerfile").Funcs(tplFuncs).Parse(rockyDockerfile)) ) type NetworkManager string @@ -102,12 +106,18 @@ func NewDockerfile(release OSRelease, img, password string, networkManager Netwo if networkManager == NetworkManagerNetplan { return d, fmt.Errorf("netplan is not supported on alpine") } - case ReleaseCentOS, ReleaseRocky, ReleaseAlmaLinux: + case ReleaseCentOS: d.tmpl = centOSDockerfileTemplate net = NetworkManagerNone if networkManager != "" && networkManager != NetworkManagerNone { return Dockerfile{}, fmt.Errorf("network manager is not supported on centos") } + case ReleaseRocky, ReleaseAlmaLinux: + d.tmpl = rockyDockerfileTemplate + net = NetworkManagerNone + if networkManager != "" && networkManager != NetworkManagerNone { + return Dockerfile{}, fmt.Errorf("network manager is not supported on rocky/almalinux") + } default: return Dockerfile{}, fmt.Errorf("unsupported distribution: %s", release.ID) } diff --git a/examples/rocky.Dockerfile b/examples/rocky.Dockerfile new file mode 100644 index 0000000..c822c0f --- /dev/null +++ b/examples/rocky.Dockerfile @@ -0,0 +1,10 @@ +FROM rockylinux/rockylinux:10 + +RUN dnf update -y +RUN dnf install -y qemu-guest-agent openssh-server && \ + echo "PermitRootLogin yes" >> /etc/ssh/sshd_config && \ + systemctl enable dbus.service && \ + systemctl set-default graphical.target + +RUN echo "NETWORKING=yes" >> /etc/sysconfig/network && \ + echo -e 'DEVICE="eth0"\nONBOOT="yes"\nBOOTPROTO="dhcp"\n' > /etc/sysconfig/network-scripts/ifcfg-eth0 diff --git a/templates/rocky.Dockerfile b/templates/rocky.Dockerfile new file mode 100644 index 0000000..a3953f0 --- /dev/null +++ b/templates/rocky.Dockerfile @@ -0,0 +1,45 @@ +FROM {{ .Image }} AS rootfs + +USER root + +# Install base packages +RUN dnf install -y \ + kernel \ + systemd \ + NetworkManager \ + e2fsprogs \ + sudo && \ + systemctl enable NetworkManager && \ + systemctl unmask systemd-remount-fs.service && \ + systemctl unmask getty.target && \ + mkdir -p /boot && \ + find /boot -type l -exec rm {} \; + +{{- if .GrubBIOS }} +RUN dnf install -y grub2 +{{- end }} +{{- if .GrubEFI }} +RUN dnf install -y grub2 grub2-efi-x64 grub2-efi-x64-modules +{{- end }} + +{{ if .Luks }} +RUN dnf install -y cryptsetup && \ + dracut --no-hostonly --regenerate-all --force --install="/usr/sbin/cryptsetup" +{{ else }} +RUN dracut --no-hostonly --regenerate-all --force +{{ end }} + +{{ if .Password }}RUN echo "root:{{ .Password }}" | chpasswd {{ end }} + +{{- if not .Grub }} +RUN cd /boot && \ + mv $(find / -name 'vmlinuz*') /boot/vmlinuz && \ + mv $(find . -name 'initramfs-*.img' -o -name initrd) /boot/initrd.img +{{- end }} + +RUN dnf clean all && \ + rm -rf /var/cache/dnf + +FROM scratch + +COPY --from=rootfs / / From e052afd2a703c8cac6b4b93a51171a6fd0cac81b Mon Sep 17 00:00:00 2001 From: Elias Abacioglu <1148206+Raboo@users.noreply.github.com> Date: Tue, 31 Mar 2026 17:03:59 +0200 Subject: [PATCH 04/20] fix: remove legacy network-scripts from Rocky example network-scripts were removed in RHEL 9+. Rocky Linux 10 uses NetworkManager exclusively, which handles DHCP on eth0 by default. --- examples/rocky.Dockerfile | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/rocky.Dockerfile b/examples/rocky.Dockerfile index c822c0f..806de66 100644 --- a/examples/rocky.Dockerfile +++ b/examples/rocky.Dockerfile @@ -5,6 +5,3 @@ RUN dnf install -y qemu-guest-agent openssh-server && \ echo "PermitRootLogin yes" >> /etc/ssh/sshd_config && \ systemctl enable dbus.service && \ systemctl set-default graphical.target - -RUN echo "NETWORKING=yes" >> /etc/sysconfig/network && \ - echo -e 'DEVICE="eth0"\nONBOOT="yes"\nBOOTPROTO="dhcp"\n' > /etc/sysconfig/network-scripts/ifcfg-eth0 From e53246b6e05a5ab5a7d04dd29fa6fc5ca1731539 Mon Sep 17 00:00:00 2001 From: Elias Abacioglu <1148206+Raboo@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:54:44 +0200 Subject: [PATCH 05/20] refactor: remove dead copyEfiBinary code from grubEFI The --removable flag passed to grub2-install already places the EFI binary at the standard removable boot path. The copyEfiBinary function was redundant and always logged a warning about not finding the source file. Remove it along with unused imports. --- grub_efi.go | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/grub_efi.go b/grub_efi.go index 009d709..a84e60d 100644 --- a/grub_efi.go +++ b/grub_efi.go @@ -17,12 +17,8 @@ package d2vm import ( "context" "fmt" - "os" - "path/filepath" "github.com/sirupsen/logrus" - - "go.linka.cloud/d2vm/pkg/exec" ) type grubEFI struct { @@ -49,44 +45,12 @@ func (g grubEFI) Setup(ctx context.Context, dev, root string, cmdline string) er if err := g.install(ctx, "--target="+g.arch+"-efi", "--efi-directory=/boot", "--no-nvram", "--removable", "--no-floppy", "--force"); err != nil { return err } - if isRhelFamily(g.r.ID) { - if err := g.copyEfiBinary(ctx, root); err != nil { - return err - } - } if err := g.mkconfig(ctx); err != nil { return err } return nil } -func (g grubEFI) copyEfiBinary(ctx context.Context, root string) error { - efiDir := filepath.Join(root, "boot", "efi", "EFI") - distroDirs := []string{"rocky", "almalinux", "centos", "rhel"} - var srcPath string - for _, d := range distroDirs { - p := filepath.Join(efiDir, d, "grub"+g.arch+".efi") - if _, err := os.Stat(p); err == nil { - srcPath = p - break - } - } - if srcPath == "" { - logrus.Warnf("could not find distro-specific EFI binary, skipping removable boot copy") - return nil - } - dstDir := filepath.Join(efiDir, "BOOT") - dstPath := filepath.Join(dstDir, "BOOT"+g.arch+".EFI") - if err := os.MkdirAll(dstDir, os.ModePerm); err != nil { - return fmt.Errorf("failed to create EFI boot directory: %w", err) - } - if err := exec.Run(ctx, "cp", srcPath, dstPath); err != nil { - return fmt.Errorf("failed to copy EFI binary to removable boot path: %w", err) - } - logrus.Infof("copied EFI binary to %s", dstPath) - return nil -} - type grubEFIProvider struct { config Config } From 9ba8941acb0255bc5fe936e42e478d705559df62 Mon Sep 17 00:00:00 2001 From: Elias Abacioglu <1148206+Raboo@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:30:46 +0200 Subject: [PATCH 06/20] fix: correct grub2-mkconfig output for RHEL-family split-boot - Add filesystem labels (rootfs, boot) to mkfs commands in builder - Add RHEL-specific grub config template with GRUB_ENABLE_BLSCFG=false and GRUB_DISABLE_LINUX_UUID=true to prevent duplicate root/ro/initrd - Write /etc/fstab with LABEL-based entries so grub2-mkconfig correctly detects separate /boot partition and generates relative paths - Set root=LABEL=rootfs in GRUB_CMDLINE_LINUX for portable root device --- builder.go | 8 ++++---- grub_common.go | 29 +++++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/builder.go b/builder.go index 8ed0910..af1c36b 100644 --- a/builder.go +++ b/builder.go @@ -334,7 +334,7 @@ func (b *builder) mountImg(ctx context.Context) error { 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 { + if err := exec.Run(ctx, "mkfs.ext4", "-L", "rootfs", b.mappedCryptRoot); err != nil { return err } if err := exec.Run(ctx, "mount", b.mappedCryptRoot, b.mntPoint); err != nil { @@ -342,7 +342,7 @@ func (b *builder) mountImg(ctx context.Context) error { } } else { logrus.Infof("creating raw image file system") - if err := exec.Run(ctx, "mkfs.ext4", b.rootPart); err != nil { + if err := exec.Run(ctx, "mkfs.ext4", "-L", "rootfs", b.rootPart); err != nil { return err } if err := exec.Run(ctx, "mount", b.rootPart, b.mntPoint); err != nil { @@ -356,9 +356,9 @@ func (b *builder) mountImg(ctx context.Context) error { return err } if b.bootFS.IsFat() { - err = exec.Run(ctx, "mkfs.fat", "-F32", b.bootPart) + err = exec.Run(ctx, "mkfs.fat", "-F32", "-n", "boot", b.bootPart) } else { - err = exec.Run(ctx, "mkfs.ext4", b.bootPart) + err = exec.Run(ctx, "mkfs.ext4", "-L", "boot", b.bootPart) } if err != nil { return err diff --git a/grub_common.go b/grub_common.go index a30fbab..00ee779 100644 --- a/grub_common.go +++ b/grub_common.go @@ -30,10 +30,18 @@ GRUB_HIDDEN_TIMEOUT=0 GRUB_HIDDEN_TIMEOUT_QUIET=true GRUB_TIMEOUT=0 GRUB_CMDLINE_LINUX_DEFAULT="%s" -GRUB_CMDLINE_LINUX="" +GRUB_CMDLINE_LINUX="root=LABEL=rootfs" GRUB_TERMINAL=console ` +const grubCfgRhel = `GRUB_DEFAULT=0 +GRUB_TIMEOUT=0 +GRUB_CMDLINE_LINUX="root=LABEL=rootfs %s" +GRUB_TERMINAL=console +GRUB_ENABLE_BLSCFG=false +GRUB_DISABLE_LINUX_UUID=true +` + type grubCommon struct { name string c Config @@ -61,9 +69,26 @@ func newGrubCommon(c Config, r OSRelease) *grubCommon { func (g *grubCommon) prepare(ctx context.Context, dev, root, cmdline string) (clean func(), err error) { g.dev = dev g.root = root - if err = os.WriteFile(filepath.Join(root, "etc", "default", "grub"), []byte(fmt.Sprintf(grubCfg, cmdline)), perm); err != nil { + + cfg := grubCfg + if isRhelFamily(g.r.ID) { + cfg = grubCfgRhel + } + if err = os.WriteFile(filepath.Join(root, "etc", "default", "grub"), []byte(fmt.Sprintf(cfg, cmdline)), perm); err != nil { return } + + // Write /etc/fstab so grub2-mkconfig detects separate /boot partition + if g.c.SplitBoot { + bootFS := "ext4" + if g.c.BootFS.IsFat() { + bootFS = "vfat" + } + fstab := fmt.Sprintf("LABEL=rootfs / ext4 defaults 0 0\nLABEL=boot /boot %s defaults 0 0\n", bootFS) + if err = os.WriteFile(filepath.Join(root, "etc", "fstab"), []byte(fstab), perm); err != nil { + return + } + } if err = os.MkdirAll(filepath.Join(root, "boot", g.name), os.ModePerm); err != nil { return } From 20d150715317a9b086328a55d0fea987f61825e2 Mon Sep 17 00:00:00 2001 From: Elias Abacioglu <1148206+Raboo@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:36:00 +0200 Subject: [PATCH 07/20] fix: generate clean grub.cfg for RHEL-family instead of using grub2-mkconfig grub2-mkconfig in a chroot environment leaks the host's /proc/cmdline (loop device paths) and produces duplicate root=, ro, and initrd= entries. Replace it with a custom grub.cfg generator for RHEL-family that: - Uses label-based boot partition lookup (search --label boot) - Uses label-based root device (root=LABEL=rootfs) - Generates correct relative paths for kernel/initrd (no /boot prefix) - Produces a single clean menuentry with no duplicates Also add SplitBoot and BootFS fields to Config struct so grubCommon can access them for template generation. --- builder.go | 3 +++ config.go | 6 +++-- grub_common.go | 62 +++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/builder.go b/builder.go index af1c36b..651d4cc 100644 --- a/builder.go +++ b/builder.go @@ -144,6 +144,9 @@ func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64, config.Initrd = strings.TrimPrefix(config.Initrd, "/boot") } + config.SplitBoot = splitBoot + config.BootFS = bootFS + if bootFS == "" { bootFS = BootFSExt4 } diff --git a/config.go b/config.go index 12da528..bcd4096 100644 --- a/config.go +++ b/config.go @@ -55,8 +55,10 @@ func (r RootPath) String() string { } type Config struct { - Kernel string - Initrd string + Kernel string + Initrd string + SplitBoot bool + BootFS BootFS } func (c Config) Cmdline(root Root, args ...string) string { diff --git a/grub_common.go b/grub_common.go index 00ee779..63898cc 100644 --- a/grub_common.go +++ b/grub_common.go @@ -19,6 +19,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/sirupsen/logrus" @@ -78,17 +79,6 @@ func (g *grubCommon) prepare(ctx context.Context, dev, root, cmdline string) (cl return } - // Write /etc/fstab so grub2-mkconfig detects separate /boot partition - if g.c.SplitBoot { - bootFS := "ext4" - if g.c.BootFS.IsFat() { - bootFS = "vfat" - } - fstab := fmt.Sprintf("LABEL=rootfs / ext4 defaults 0 0\nLABEL=boot /boot %s defaults 0 0\n", bootFS) - if err = os.WriteFile(filepath.Join(root, "etc", "fstab"), []byte(fstab), perm); err != nil { - return - } - } if err = os.MkdirAll(filepath.Join(root, "boot", g.name), os.ModePerm); err != nil { return } @@ -127,5 +117,55 @@ func (g *grubCommon) mkconfig(ctx context.Context) error { if g.dev == "" || g.root == "" { return fmt.Errorf("grubCommon not prepared") } + if isRhelFamily(g.r.ID) { + return g.mkconfigRhel(ctx) + } return exec.Run(ctx, "chroot", g.root, g.name+"-mkconfig", "-o", "/boot/"+g.name+"/grub.cfg") } + +func (g *grubCommon) mkconfigRhel(ctx context.Context) error { + // Find kernel and initrd + kernelPattern := filepath.Join(g.root, "boot", "vmlinuz-*") + matches, err := filepath.Glob(kernelPattern) + if err != nil { + return fmt.Errorf("failed to find kernel: %w", err) + } + if len(matches) == 0 { + return fmt.Errorf("no kernel found in /boot") + } + // Pick the latest kernel + kernel := matches[len(matches)-1] + kernelName := filepath.Base(kernel) + + initrdPattern := filepath.Join(g.root, "boot", "initramfs-*.img") + initrdMatches, err := filepath.Glob(initrdPattern) + if err != nil { + return fmt.Errorf("failed to find initrd: %w", err) + } + if len(initrdMatches) == 0 { + return fmt.Errorf("no initrd found in /boot") + } + initrd := initrdMatches[len(initrdMatches)-1] + initrdName := filepath.Base(initrd) + + // Extract version from kernel name + version := strings.TrimPrefix(kernelName, "vmlinuz-") + + cfg := fmt.Sprintf(`# Generated by d2vm +set default="0" +set timeout=0 + +insmod part_msdos +insmod fat +search --no-floppy --label --set=root boot + +menuentry 'Rocky Linux %s' --class gnu-linux --class gnu --class os { + load_video + insmod gzio + linux /%s root=LABEL=rootfs ro net.ifnames=0 rootfstype=ext4 console=tty0 console=ttyS0,115200n8 + initrd /%s +} +`, version, kernelName, initrdName) + + return os.WriteFile(filepath.Join(g.root, "boot", g.name, "grub.cfg"), []byte(cfg), perm) +} From a2e47dc5ccf33dbe35df81c2fb2f86ff92586c11 Mon Sep 17 00:00:00 2001 From: Elias Abacioglu <1148206+Raboo@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:42:10 +0200 Subject: [PATCH 08/20] refactor: remove unused grubCfgRhel constant and conditional logic Since we now generate grub.cfg directly for RHEL-family instead of using grub2-mkconfig, the RHEL-specific /etc/default/grub template is no longer needed. Simplify prepare() to use the standard template for all distros. --- grub_common.go | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/grub_common.go b/grub_common.go index 63898cc..e9bc426 100644 --- a/grub_common.go +++ b/grub_common.go @@ -35,14 +35,6 @@ GRUB_CMDLINE_LINUX="root=LABEL=rootfs" GRUB_TERMINAL=console ` -const grubCfgRhel = `GRUB_DEFAULT=0 -GRUB_TIMEOUT=0 -GRUB_CMDLINE_LINUX="root=LABEL=rootfs %s" -GRUB_TERMINAL=console -GRUB_ENABLE_BLSCFG=false -GRUB_DISABLE_LINUX_UUID=true -` - type grubCommon struct { name string c Config @@ -71,11 +63,7 @@ func (g *grubCommon) prepare(ctx context.Context, dev, root, cmdline string) (cl g.dev = dev g.root = root - cfg := grubCfg - if isRhelFamily(g.r.ID) { - cfg = grubCfgRhel - } - if err = os.WriteFile(filepath.Join(root, "etc", "default", "grub"), []byte(fmt.Sprintf(cfg, cmdline)), perm); err != nil { + if err = os.WriteFile(filepath.Join(root, "etc", "default", "grub"), []byte(fmt.Sprintf(grubCfg, cmdline)), perm); err != nil { return } From 3bdd92b31294e17fc0e538855f2be5b477b8f361 Mon Sep 17 00:00:00 2001 From: Elias Abacioglu <1148206+Raboo@users.noreply.github.com> Date: Thu, 2 Apr 2026 14:40:04 +0200 Subject: [PATCH 09/20] fix: set grub timeout to 5s and remove load_video for RHEL-family - Set timeout=5 so users can interrupt boot to edit grub entries - Remove load_video which fails with 'can't find command' error in minimal grub environment and is unnecessary for serial console VMs --- grub_common.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/grub_common.go b/grub_common.go index e9bc426..5f36d8b 100644 --- a/grub_common.go +++ b/grub_common.go @@ -141,14 +141,13 @@ func (g *grubCommon) mkconfigRhel(ctx context.Context) error { cfg := fmt.Sprintf(`# Generated by d2vm set default="0" -set timeout=0 +set timeout=5 insmod part_msdos insmod fat search --no-floppy --label --set=root boot menuentry 'Rocky Linux %s' --class gnu-linux --class gnu --class os { - load_video insmod gzio linux /%s root=LABEL=rootfs ro net.ifnames=0 rootfstype=ext4 console=tty0 console=ttyS0,115200n8 initrd /%s From f27c011d9e688959d491552023bbe07e4d133a98 Mon Sep 17 00:00:00 2001 From: Elias Abacioglu <1148206+Raboo@users.noreply.github.com> Date: Tue, 7 Apr 2026 15:58:16 +0200 Subject: [PATCH 10/20] feat: add SELinux autorelabel trigger for RHEL-family distros Create /.autorelabel during image build so the first boot triggers a full filesystem relabel. This is required because SELinux contexts from the Docker build don't match the policy loaded at boot time. --- grub_common.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/grub_common.go b/grub_common.go index 5f36d8b..fe35309 100644 --- a/grub_common.go +++ b/grub_common.go @@ -67,6 +67,15 @@ func (g *grubCommon) prepare(ctx context.Context, dev, root, cmdline string) (cl return } + // Trigger SELinux relabel on first boot for RHEL-family distros. + // The filesystem contexts from the Docker build don't match the + // policy loaded at boot, so a relabel is required. + if isRhelFamily(g.r.ID) { + if err = os.WriteFile(filepath.Join(root, ".autorelabel"), []byte{}, perm); err != nil { + return + } + } + if err = os.MkdirAll(filepath.Join(root, "boot", g.name), os.ModePerm); err != nil { return } From 7b04fe736c82697000f0dfaaef50ee2ef5fa16f1 Mon Sep 17 00:00:00 2001 From: Elias Abacioglu <1148206+Raboo@users.noreply.github.com> Date: Fri, 24 Apr 2026 15:13:15 +0200 Subject: [PATCH 11/20] remove unused SplitBoot and BootFS from Config struct These fields were assigned but never read. The builder struct has its own splitBoot and bootFS fields that are used throughout the codebase. --- builder.go | 3 --- config.go | 6 ++---- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/builder.go b/builder.go index 651d4cc..af1c36b 100644 --- a/builder.go +++ b/builder.go @@ -144,9 +144,6 @@ func NewBuilder(ctx context.Context, workdir, imgTag, disk string, size uint64, config.Initrd = strings.TrimPrefix(config.Initrd, "/boot") } - config.SplitBoot = splitBoot - config.BootFS = bootFS - if bootFS == "" { bootFS = BootFSExt4 } diff --git a/config.go b/config.go index bcd4096..12da528 100644 --- a/config.go +++ b/config.go @@ -55,10 +55,8 @@ func (r RootPath) String() string { } type Config struct { - Kernel string - Initrd string - SplitBoot bool - BootFS BootFS + Kernel string + Initrd string } func (c Config) Cmdline(root Root, args ...string) string { From 34f553ddf4fc55b1dbfcb73b4bb4996665927193 Mon Sep 17 00:00:00 2001 From: Elias Abacioglu <1148206+Raboo@users.noreply.github.com> Date: Fri, 24 Apr 2026 15:48:06 +0200 Subject: [PATCH 12/20] revert: restore individual LUKS cases for RHEL-family distros in os_release.go This change was out of scope for this PR. --- os_release.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/os_release.go b/os_release.go index ad7f586..a1088bc 100644 --- a/os_release.go +++ b/os_release.go @@ -85,7 +85,11 @@ func (r OSRelease) SupportsLUKS() bool { return true case ReleaseAlpine: return true - case ReleaseCentOS, ReleaseRocky, ReleaseAlmaLinux: + case ReleaseCentOS: + return true + case ReleaseRocky: + return true + case ReleaseAlmaLinux: return true case ReleaseRHEL: return false From 8629364bac157514def85b9560f8fa18ec7ea5e8 Mon Sep 17 00:00:00 2001 From: Elias Abacioglu <1148206+Raboo@users.noreply.github.com> Date: Fri, 24 Apr 2026 15:59:27 +0200 Subject: [PATCH 13/20] revert: restore LUKS cases order in os_release.go --- os_release.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/os_release.go b/os_release.go index a1088bc..a0f7197 100644 --- a/os_release.go +++ b/os_release.go @@ -83,14 +83,14 @@ func (r OSRelease) SupportsLUKS() bool { case ReleaseKali: // TODO: check version return true - case ReleaseAlpine: - return true case ReleaseCentOS: return true case ReleaseRocky: return true case ReleaseAlmaLinux: return true + case ReleaseAlpine: + return true case ReleaseRHEL: return false default: From b5fcb2b77b938a583f684a3f8f7eb0b919f2a1a9 Mon Sep 17 00:00:00 2001 From: Elias Abacioglu <1148206+Raboo@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:09:30 +0200 Subject: [PATCH 14/20] e2e: enable EFI testing for RHEL-family distros Remove the skip for centos/almalinux/rocky EFI tests now that grub2-install with --force works in chroot environments. --- e2e/e2e_test.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index d349158..80e58fd 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -120,9 +120,6 @@ imgs: defer os.RemoveAll(dir) for _, img := range testImgs { - if (strings.Contains(img.name, "centos") || strings.Contains(img.name, "almalinux") || strings.Contains(img.name, "rocky")) && tt.efi { - t.Skip("efi not supported for CentOS") - } t.Run(img.name, func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() From 0533e42e229957cd82a5d6ef449af48e4ccf169b Mon Sep 17 00:00:00 2001 From: Elias Abacioglu <1148206+Raboo@users.noreply.github.com> Date: Mon, 27 Apr 2026 10:47:44 +0200 Subject: [PATCH 15/20] unify/consolidate RHEL-family Dockerfile templates into CentOS. Remove the dedicated rocky.Dockerfile template and instead use the centOS template for Rocky Linux and AlmaLinux releases. This reduces template duplication as the base package installations and configurations are now unified across these RHEL-family distributions. --- dockerfile.go | 12 +--------- templates/rocky.Dockerfile | 45 -------------------------------------- 2 files changed, 1 insertion(+), 56 deletions(-) delete mode 100644 templates/rocky.Dockerfile diff --git a/dockerfile.go b/dockerfile.go index cb9afc5..4f91319 100644 --- a/dockerfile.go +++ b/dockerfile.go @@ -36,15 +36,11 @@ var alpineDockerfile string //go:embed templates/centos.Dockerfile var centOSDockerfile string -//go:embed templates/rocky.Dockerfile -var rockyDockerfile string - var ( ubuntuDockerfileTemplate = template.Must(template.New("ubuntu.Dockerfile").Funcs(tplFuncs).Parse(ubuntuDockerfile)) debianDockerfileTemplate = template.Must(template.New("debian.Dockerfile").Funcs(tplFuncs).Parse(debianDockerfile)) alpineDockerfileTemplate = template.Must(template.New("alpine.Dockerfile").Funcs(tplFuncs).Parse(alpineDockerfile)) centOSDockerfileTemplate = template.Must(template.New("centos.Dockerfile").Funcs(tplFuncs).Parse(centOSDockerfile)) - rockyDockerfileTemplate = template.Must(template.New("rocky.Dockerfile").Funcs(tplFuncs).Parse(rockyDockerfile)) ) type NetworkManager string @@ -106,18 +102,12 @@ func NewDockerfile(release OSRelease, img, password string, networkManager Netwo if networkManager == NetworkManagerNetplan { return d, fmt.Errorf("netplan is not supported on alpine") } - case ReleaseCentOS: + case ReleaseCentOS, ReleaseRocky, ReleaseAlmaLinux: d.tmpl = centOSDockerfileTemplate net = NetworkManagerNone if networkManager != "" && networkManager != NetworkManagerNone { return Dockerfile{}, fmt.Errorf("network manager is not supported on centos") } - case ReleaseRocky, ReleaseAlmaLinux: - d.tmpl = rockyDockerfileTemplate - net = NetworkManagerNone - if networkManager != "" && networkManager != NetworkManagerNone { - return Dockerfile{}, fmt.Errorf("network manager is not supported on rocky/almalinux") - } default: return Dockerfile{}, fmt.Errorf("unsupported distribution: %s", release.ID) } diff --git a/templates/rocky.Dockerfile b/templates/rocky.Dockerfile deleted file mode 100644 index a3953f0..0000000 --- a/templates/rocky.Dockerfile +++ /dev/null @@ -1,45 +0,0 @@ -FROM {{ .Image }} AS rootfs - -USER root - -# Install base packages -RUN dnf install -y \ - kernel \ - systemd \ - NetworkManager \ - e2fsprogs \ - sudo && \ - systemctl enable NetworkManager && \ - systemctl unmask systemd-remount-fs.service && \ - systemctl unmask getty.target && \ - mkdir -p /boot && \ - find /boot -type l -exec rm {} \; - -{{- if .GrubBIOS }} -RUN dnf install -y grub2 -{{- end }} -{{- if .GrubEFI }} -RUN dnf install -y grub2 grub2-efi-x64 grub2-efi-x64-modules -{{- end }} - -{{ if .Luks }} -RUN dnf install -y cryptsetup && \ - dracut --no-hostonly --regenerate-all --force --install="/usr/sbin/cryptsetup" -{{ else }} -RUN dracut --no-hostonly --regenerate-all --force -{{ end }} - -{{ if .Password }}RUN echo "root:{{ .Password }}" | chpasswd {{ end }} - -{{- if not .Grub }} -RUN cd /boot && \ - mv $(find / -name 'vmlinuz*') /boot/vmlinuz && \ - mv $(find . -name 'initramfs-*.img' -o -name initrd) /boot/initrd.img -{{- end }} - -RUN dnf clean all && \ - rm -rf /var/cache/dnf - -FROM scratch - -COPY --from=rootfs / / From e5d21cd57a0da92998261d4965ddbd03c8e29b46 Mon Sep 17 00:00:00 2001 From: Elias Abacioglu <1148206+Raboo@users.noreply.github.com> Date: Tue, 28 Apr 2026 09:43:19 +0200 Subject: [PATCH 16/20] config: pass OSRelease to Cmdline for RHEL-family kernel cmdline format Instead of adding a field to Config, Cmdline now takes OSRelease and calls isRhelFamily() to choose the correct kernel cmdline format. RHEL-family distros omit 'ro initrd=...' prefix; others keep it. --- builder.go | 8 ++++---- config.go | 11 +++++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/builder.go b/builder.go index af1c36b..3e3c4f3 100644 --- a/builder.go +++ b/builder.go @@ -466,18 +466,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(b.osRelease, RootUUID(b.rootUUID), 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(b.osRelease, RootUUID(b.rootUUID), "root=/dev/mapper/root", "cryptdm=root", "cryptroot=UUID="+b.cryptUUID, b.cmdLineExtra) case ReleaseCentOS, ReleaseRocky, ReleaseAlmaLinux: - 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(b.osRelease, RootUUID(b.rootUUID), "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(b.osRelease, nil, "root=/dev/mapper/root", "cryptopts=target=root,source=UUID="+b.cryptUUID+",key=none,luks", b.cmdLineExtra) } } diff --git a/config.go b/config.go index 12da528..e091e16 100644 --- a/config.go +++ b/config.go @@ -59,12 +59,15 @@ type Config struct { Initrd string } -func (c Config) Cmdline(root Root, args ...string) string { - var r string +func (c Config) Cmdline(r OSRelease, root Root, args ...string) string { + var rootStr string if root != nil { - r = fmt.Sprintf("root=%s", root.String()) + rootStr = 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, " ")) + if isRhelFamily(r.ID) { + return fmt.Sprintf("net.ifnames=0 rootfstype=ext4 console=tty0 console=ttyS0,115200n8 %s", strings.Join(args, " ")) + } + return fmt.Sprintf("ro initrd=%s %s net.ifnames=0 rootfstype=ext4 console=tty0 console=ttyS0,115200n8 %s", c.Initrd, rootStr, strings.Join(args, " ")) } func (r OSRelease) Config() (Config, error) { From 053afffaaf103a25ad5c757ffa4836285aef851f Mon Sep 17 00:00:00 2001 From: Elias Abacioglu <1148206+Raboo@users.noreply.github.com> Date: Tue, 28 Apr 2026 15:03:48 +0200 Subject: [PATCH 17/20] e2e: add more Rocky Linux images to the test suite Include Docker Inc's official images and Rocky Linux Project official images for versions 9 and 10. --- e2e/e2e_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index 80e58fd..dc1963b 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -55,7 +55,10 @@ var ( {name: "centos:8", luks: "Please enter passphrase for disk"}, {name: "quay.io/centos/centos:stream10", luks: "Please enter passphrase for disk"}, {name: "almalinux:10", luks: "Please enter passphrase for disk"}, - {name: "rockylinux:9", luks: "Please enter passphrase for disk"}, + {name: "rockylinux:9", luks: "Please enter passphrase for disk"}, // Docker Inc's official Rocky image + {name: "rockylinux:10", luks: "Please enter passphrase for disk"}, // Docker Inc's official Rocky image + {name: "rockylinux/rockylinux:9", luks: "Please enter passphrase for disk"}, // Rocky Linux Project official image + {name: "rockylinux/rockylinux:10", luks: "Please enter passphrase for disk"}, // Rocky Linux Project official image } imgNames = func() []string { var imgs []string From 93e205472dd2403114c7c560ba8ba1eb89ad3093 Mon Sep 17 00:00:00 2001 From: Elias Abacioglu <1148206+Raboo@users.noreply.github.com> Date: Tue, 28 Apr 2026 15:04:16 +0200 Subject: [PATCH 18/20] grub: use grub2-mkconfig for RHEL-family and adjust grubCfg - Remove custom mkconfigRhel, fall back to grub2-mkconfig - Set GRUB_TIMEOUT=1 to allow editing boot entries - Remove root=LABEL=rootfs from GRUB_CMDLINE_LINUX (passed via cmdline) --- grub_common.go | 56 +++----------------------------------------------- 1 file changed, 3 insertions(+), 53 deletions(-) diff --git a/grub_common.go b/grub_common.go index fe35309..0affcfb 100644 --- a/grub_common.go +++ b/grub_common.go @@ -19,7 +19,6 @@ import ( "fmt" "os" "path/filepath" - "strings" "github.com/sirupsen/logrus" @@ -29,9 +28,10 @@ import ( const grubCfg = `GRUB_DEFAULT=0 GRUB_HIDDEN_TIMEOUT=0 GRUB_HIDDEN_TIMEOUT_QUIET=true -GRUB_TIMEOUT=0 +# 1 second grub countdown so we can handle boot issues +GRUB_TIMEOUT=1 GRUB_CMDLINE_LINUX_DEFAULT="%s" -GRUB_CMDLINE_LINUX="root=LABEL=rootfs" +GRUB_CMDLINE_LINUX="" GRUB_TERMINAL=console ` @@ -62,7 +62,6 @@ func newGrubCommon(c Config, r OSRelease) *grubCommon { func (g *grubCommon) prepare(ctx context.Context, dev, root, cmdline string) (clean func(), err error) { g.dev = dev g.root = root - if err = os.WriteFile(filepath.Join(root, "etc", "default", "grub"), []byte(fmt.Sprintf(grubCfg, cmdline)), perm); err != nil { return } @@ -114,54 +113,5 @@ func (g *grubCommon) mkconfig(ctx context.Context) error { if g.dev == "" || g.root == "" { return fmt.Errorf("grubCommon not prepared") } - if isRhelFamily(g.r.ID) { - return g.mkconfigRhel(ctx) - } return exec.Run(ctx, "chroot", g.root, g.name+"-mkconfig", "-o", "/boot/"+g.name+"/grub.cfg") } - -func (g *grubCommon) mkconfigRhel(ctx context.Context) error { - // Find kernel and initrd - kernelPattern := filepath.Join(g.root, "boot", "vmlinuz-*") - matches, err := filepath.Glob(kernelPattern) - if err != nil { - return fmt.Errorf("failed to find kernel: %w", err) - } - if len(matches) == 0 { - return fmt.Errorf("no kernel found in /boot") - } - // Pick the latest kernel - kernel := matches[len(matches)-1] - kernelName := filepath.Base(kernel) - - initrdPattern := filepath.Join(g.root, "boot", "initramfs-*.img") - initrdMatches, err := filepath.Glob(initrdPattern) - if err != nil { - return fmt.Errorf("failed to find initrd: %w", err) - } - if len(initrdMatches) == 0 { - return fmt.Errorf("no initrd found in /boot") - } - initrd := initrdMatches[len(initrdMatches)-1] - initrdName := filepath.Base(initrd) - - // Extract version from kernel name - version := strings.TrimPrefix(kernelName, "vmlinuz-") - - cfg := fmt.Sprintf(`# Generated by d2vm -set default="0" -set timeout=5 - -insmod part_msdos -insmod fat -search --no-floppy --label --set=root boot - -menuentry 'Rocky Linux %s' --class gnu-linux --class gnu --class os { - insmod gzio - linux /%s root=LABEL=rootfs ro net.ifnames=0 rootfstype=ext4 console=tty0 console=ttyS0,115200n8 - initrd /%s -} -`, version, kernelName, initrdName) - - return os.WriteFile(filepath.Join(g.root, "boot", g.name, "grub.cfg"), []byte(cfg), perm) -} From a85b36ef466bafae3d70876c8a829fae136ad346 Mon Sep 17 00:00:00 2001 From: Elias Abacioglu <1148206+Raboo@users.noreply.github.com> Date: Tue, 28 Apr 2026 15:04:47 +0200 Subject: [PATCH 19/20] builder: add fixLoaderEntries to fix split-boot loader paths For split-boot setups, strip /boot/ prefix from paths in /boot/loader/entries/ files since the boot partition is mounted at / at runtime. --- builder.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/builder.go b/builder.go index 3e3c4f3..16a5346 100644 --- a/builder.go +++ b/builder.go @@ -445,6 +445,14 @@ func (b *builder) setupRootFS(ctx context.Context) (err error) { return err } + // Fix /boot/loader/entries/ files for split-boot: remove /boot prefix + // from paths since the boot partition (GRUB Root) is accessed as / initially. + if b.splitBoot { + if err := b.fixLoaderEntries(); err != nil { + return err + } + } + switch b.osRelease.ID { case ReleaseAlpine: by, err := os.ReadFile(b.chPath("/etc/inittab")) @@ -481,6 +489,34 @@ func (b *builder) cmdline(_ context.Context) string { } } +func (b *builder) fixLoaderEntries() error { + entriesDir := b.chPath("/boot/loader/entries") + entries, err := os.ReadDir(entriesDir) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + for _, entry := range entries { + if entry.IsDir() { + continue + } + path := filepath.Join(entriesDir, entry.Name()) + content, err := os.ReadFile(path) + if err != nil { + return err + } + fixed := strings.ReplaceAll(string(content), "/boot/", "/") + if fixed != string(content) { + if err := os.WriteFile(path, []byte(fixed), 0644); err != nil { + return err + } + } + } + return nil +} + func (b *builder) installBootloader(ctx context.Context) error { logrus.Infof("installing bootloader") return b.bootloader.Setup(ctx, b.loDevice, b.mntPoint, b.cmdline(ctx)) From e1350551808827da7dc674ae5762aa0f90d2fb52 Mon Sep 17 00:00:00 2001 From: Elias Abacioglu <1148206+Raboo@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:15:09 +0200 Subject: [PATCH 20/20] e2e: "rockylinux:10" does not exist. --- e2e/e2e_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index dc1963b..40aa855 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -56,7 +56,6 @@ var ( {name: "quay.io/centos/centos:stream10", luks: "Please enter passphrase for disk"}, {name: "almalinux:10", luks: "Please enter passphrase for disk"}, {name: "rockylinux:9", luks: "Please enter passphrase for disk"}, // Docker Inc's official Rocky image - {name: "rockylinux:10", luks: "Please enter passphrase for disk"}, // Docker Inc's official Rocky image {name: "rockylinux/rockylinux:9", luks: "Please enter passphrase for disk"}, // Rocky Linux Project official image {name: "rockylinux/rockylinux:10", luks: "Please enter passphrase for disk"}, // Rocky Linux Project official image }