Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
bb9657d
feat: add RHEL 10 GRUB support with grub2 commands and EFI binary han…
Raboo Mar 31, 2026
ace561f
fix: add --force flag to grub2-install for EFI platforms in chroot
Raboo Mar 31, 2026
7825993
feat: add Rocky Linux template and example
Raboo Mar 31, 2026
e052afd
fix: remove legacy network-scripts from Rocky example
Raboo Mar 31, 2026
e53246b
refactor: remove dead copyEfiBinary code from grubEFI
Raboo Apr 1, 2026
9ba8941
fix: correct grub2-mkconfig output for RHEL-family split-boot
Raboo Apr 1, 2026
20d1507
fix: generate clean grub.cfg for RHEL-family instead of using grub2-m…
Raboo Apr 1, 2026
a2e47dc
refactor: remove unused grubCfgRhel constant and conditional logic
Raboo Apr 1, 2026
3bdd92b
fix: set grub timeout to 5s and remove load_video for RHEL-family
Raboo Apr 2, 2026
f27c011
feat: add SELinux autorelabel trigger for RHEL-family distros
Raboo Apr 7, 2026
7b04fe7
remove unused SplitBoot and BootFS from Config struct
Raboo Apr 24, 2026
34f553d
revert: restore individual LUKS cases for RHEL-family distros in os_r…
Raboo Apr 24, 2026
8629364
revert: restore LUKS cases order in os_release.go
Raboo Apr 24, 2026
b5fcb2b
e2e: enable EFI testing for RHEL-family distros
Raboo Apr 24, 2026
0533e42
unify/consolidate RHEL-family Dockerfile templates into CentOS.
Raboo Apr 27, 2026
e5d21cd
config: pass OSRelease to Cmdline for RHEL-family kernel cmdline format
Raboo Apr 28, 2026
053afff
e2e: add more Rocky Linux images to the test suite
Raboo Apr 28, 2026
93e2054
grub: use grub2-mkconfig for RHEL-family and adjust grubCfg
Raboo Apr 28, 2026
a85b36e
builder: add fixLoaderEntries to fix split-boot loader paths
Raboo Apr 28, 2026
e135055
e2e: "rockylinux:10" does not exist.
Raboo Apr 29, 2026
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
52 changes: 44 additions & 8 deletions builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -334,15 +334,15 @@ 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 {
return err
}
} 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 {
Expand All @@ -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
Expand Down Expand Up @@ -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"))
Expand All @@ -466,19 +474,47 @@ 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)
}
}

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 {
Expand Down
11 changes: 7 additions & 4 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
7 changes: 3 additions & 4 deletions e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ 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/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
Expand Down Expand Up @@ -120,9 +122,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()
Expand Down
7 changes: 7 additions & 0 deletions examples/rocky.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
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
5 changes: 1 addition & 4 deletions grub.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}

Expand Down
19 changes: 17 additions & 2 deletions grub_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ 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=""
GRUB_TERMINAL=console
Expand All @@ -42,9 +43,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{
Expand All @@ -60,6 +65,16 @@ func (g *grubCommon) prepare(ctx context.Context, dev, root, cmdline string) (cl
if err = os.WriteFile(filepath.Join(root, "etc", "default", "grub"), []byte(fmt.Sprintf(grubCfg, cmdline)), perm); err != nil {
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
}
Expand Down
5 changes: 1 addition & 4 deletions grub_efi.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,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 err := g.mkconfig(ctx); err != nil {
Expand All @@ -56,9 +56,6 @@ type grubEFIProvider struct {
}

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
}

Expand Down