diff --git a/builder.go b/builder.go index 8ed0910..16a5346 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 @@ -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")) @@ -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 { 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) { diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go index d349158..40aa855 100644 --- a/e2e/e2e_test.go +++ b/e2e/e2e_test.go @@ -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 @@ -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() diff --git a/examples/rocky.Dockerfile b/examples/rocky.Dockerfile new file mode 100644 index 0000000..806de66 --- /dev/null +++ b/examples/rocky.Dockerfile @@ -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 diff --git a/grub.go b/grub.go index e853f4a..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 { @@ -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..0affcfb 100644 --- a/grub_common.go +++ b/grub_common.go @@ -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 @@ -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{ @@ -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 } diff --git a/grub_efi.go b/grub_efi.go index 4c3a83f..a84e60d 100644 --- a/grub_efi.go +++ b/grub_efi.go @@ -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 { @@ -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 }