From 5c34857bd4e07e3016306684c4e667def0b08545 Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Mon, 21 Sep 2026 09:26:50 +0200 Subject: [PATCH 1/2] bootc: parse image type container info and propagate This supports parsing the new `Type` field from from `bootc container inspect` and propagates to BootcDistro, BbootcDiskImage and RawBootcImage. Signed-off-by: Alexander Larsson --- pkg/bootc/bootc.go | 21 +++++++++++++++++++++ pkg/bootc/resolver.go | 25 +++++++++++++++++++------ pkg/bootc/resolver_test.go | 18 ++++++++++++++++++ pkg/distro/defs/bootc.go | 2 ++ pkg/distro/defs/bootc_imagetype.go | 1 + pkg/distro/defs/bootc_test.go | 27 ++++++++++++++++++++++++--- pkg/image/bootc_disk.go | 2 ++ pkg/manifest/raw_bootc.go | 1 + 8 files changed, 88 insertions(+), 9 deletions(-) diff --git a/pkg/bootc/bootc.go b/pkg/bootc/bootc.go index e992cd686e..e5ed03c7f4 100644 --- a/pkg/bootc/bootc.go +++ b/pkg/bootc/bootc.go @@ -4,6 +4,24 @@ package bootc import "github.com/osbuild/image-builder/pkg/bib/osinfo" +// ContainerType is the boot artifact type reported by bootc container inspect. +type ContainerType string + +const ( + ContainerTypeAboot ContainerType = "aboot" + ContainerTypeAbootEFI ContainerType = "aboot-efi" +) + +// IsAboot reports whether the type describes an aboot partition payload. +func (t ContainerType) IsAboot() bool { + return t == ContainerTypeAboot || t == ContainerTypeAbootEFI +} + +// IsAbootEFI reports whether the aboot payload requires an ESP for ukiboot. +func (t ContainerType) IsAbootEFI() bool { + return t == ContainerTypeAbootEFI +} + // Info contains all the information from the bootc container that is // required to create a manifest for a bootc-based image. type Info struct { @@ -28,6 +46,9 @@ type Info struct { // Is the container using a unified kernel? UnifiedKernel bool + // The boot artifact type reported by the container. + ContainerType ContainerType + // What bootloader should be passed? Bootloader *string } diff --git a/pkg/bootc/resolver.go b/pkg/bootc/resolver.go index c223543655..e5bac8cca3 100644 --- a/pkg/bootc/resolver.go +++ b/pkg/bootc/resolver.go @@ -235,11 +235,12 @@ func (c *Container) ResolveInfo(variant string) (*Info, error) { bootcInfo.DefaultRootFs = bootcInstallConfig.Filesystem.Root.Type bootcInfo.Bootloader = bootcInstallConfig.Bootloader - unifiedKernel, err := c.UnifiedKernel() + unifiedKernel, containerType, err := c.bootArtifacts() if err != nil { return nil, err } bootcInfo.UnifiedKernel = unifiedKernel + bootcInfo.ContainerType = containerType size, err := getContainerSize(c.ref, c.storeOpts) if err != nil { @@ -384,6 +385,17 @@ func (c *Container) InitrdModules(kver string) ([]string, error) { // UnifiedKernel finds out if the kernel inside the bootc container is unified func (c *Container) UnifiedKernel() (bool, error) { + unified, _, err := c.bootArtifacts() + return unified, err +} + +// HasAboot finds out if the bootc container has an aboot image. +func (c *Container) HasAboot() (bool, error) { + _, containerType, err := c.bootArtifacts() + return containerType.IsAboot(), err +} + +func (c *Container) bootArtifacts() (bool, ContainerType, error) { args := []string{"exec"} args = append(args, c.storeOpts...) args = append(args, c.id, "bootc", "container", "inspect", "--json") @@ -394,13 +406,14 @@ func (c *Container) UnifiedKernel() (bool, error) { if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 2 { // NOTE: the 'bootc container inspect' was added in version 1.12.0, which was not in RHEL-10.1. // Treat exit value '2' as a non-error, since it means the command is not available and return false. - return false, nil + return false, "", nil } - return false, fmt.Errorf("failed to run bootc container inspect: %w, output:\n%s", err, output) + return false, "", fmt.Errorf("failed to run bootc container inspect: %w, output:\n%s", err, output) } var bootcInspect struct { - Kargs []string `json:"kargs"` + Type ContainerType `json:"type"` + Kargs []string `json:"kargs"` Kernel struct { Version string `json:"version"` Unified bool `json:"unified"` @@ -408,10 +421,10 @@ func (c *Container) UnifiedKernel() (bool, error) { } if err := json.Unmarshal(output, &bootcInspect); err != nil { - return false, fmt.Errorf("failed to unmarshal bootc inspect : %w", err) + return false, "", fmt.Errorf("failed to unmarshal bootc inspect : %w", err) } - return bootcInspect.Kernel.Unified, nil + return bootcInspect.Kernel.Unified, bootcInspect.Type, nil } func findImageIdFor(cntId, ref string, extraOpts []string) (string, error) { diff --git a/pkg/bootc/resolver_test.go b/pkg/bootc/resolver_test.go index 20a5862ad5..d3a0574856 100644 --- a/pkg/bootc/resolver_test.go +++ b/pkg/bootc/resolver_test.go @@ -259,6 +259,24 @@ func TestUnifiedKernelHappy(t *testing.T) { } } +func TestHasAboot(t *testing.T) { + for _, tc := range []struct { + in string + out bool + }{ + {`{"type": "aboot"}`, true}, + {`{"type": "aboot-efi"}`, true}, + {`{"type": "uki"}`, false}, + {`{}`, false}, + } { + makeFakePodman(t, fmt.Sprintf("#!/bin/sh\necho '%s'\n", tc.in)) + cnt := bootc.Container{} + aboot, err := cnt.HasAboot() + assert.NoError(t, err) + assert.Equal(t, tc.out, aboot) + } +} + func TestBootloaderHappy(t *testing.T) { for _, tc := range []struct { In string diff --git a/pkg/distro/defs/bootc.go b/pkg/distro/defs/bootc.go index c5324df18d..d84e0c584b 100644 --- a/pkg/distro/defs/bootc.go +++ b/pkg/distro/defs/bootc.go @@ -25,6 +25,7 @@ type BootcDistro struct { sourceInfo *osinfo.Info buildSourceInfo *osinfo.Info unifiedKernel bool + containerType bootc.ContainerType bootloader *string id distro.ID @@ -140,6 +141,7 @@ func NewBootcWithLoader(loader *Loader, name string, cinfo *bootc.Info) (*BootcD rootfsMinSize: cinfo.Size * containerSizeToDiskSizeMultiplier, bootloader: cinfo.Bootloader, unifiedKernel: cinfo.UnifiedKernel, + containerType: cinfo.ContainerType, } // load image types from bootc-generic-1 diff --git a/pkg/distro/defs/bootc_imagetype.go b/pkg/distro/defs/bootc_imagetype.go index f7dfeab9b5..d5a5f83863 100644 --- a/pkg/distro/defs/bootc_imagetype.go +++ b/pkg/distro/defs/bootc_imagetype.go @@ -266,6 +266,7 @@ func (t *bootcImageType) manifestForDisk(bp *blueprint.Blueprint, options distro img.Bootloader = bd.bootloader img.UnifiedKernel = bd.unifiedKernel + img.Aboot = bd.containerType.IsAboot() img.OSCustomizations.Subscription = options.Subscription img.OSCustomizations.Users = users.UsersFromBP(customizations.GetUsers()) diff --git a/pkg/distro/defs/bootc_test.go b/pkg/distro/defs/bootc_test.go index 60dac23ab9..d2573369db 100644 --- a/pkg/distro/defs/bootc_test.go +++ b/pkg/distro/defs/bootc_test.go @@ -23,6 +23,7 @@ import ( "github.com/osbuild/image-builder/pkg/manifestgen/manifestmock" "github.com/osbuild/image-builder/pkg/osbuild" "github.com/osbuild/image-builder/pkg/osbuild/manifesttest" + "github.com/osbuild/image-builder/pkg/platform" "github.com/osbuild/image-builder/pkg/rpmmd" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -42,6 +43,7 @@ func TestNewBootc(t *testing.T) { "ok": { info: &bootc.Info{ + ContainerType: bootc.ContainerTypeAbootEFI, Imgref: "example.com/containers/distro-bootc:version12", ImageID: "acf88e518194fac963a1b2e2e4110e38a4ce5fb3fceddd624fae8997d4566930", Arch: "arm64", @@ -55,9 +57,10 @@ func TestNewBootc(t *testing.T) { }, }, expectedDistro: &BootcDistro{ - imgref: "example.com/containers/distro-bootc:version12", - imageID: "acf88e518194fac963a1b2e2e4110e38a4ce5fb3fceddd624fae8997d4566930", - buildImgref: "example.com/containers/distro-bootc:version12", + containerType: bootc.ContainerTypeAbootEFI, + imgref: "example.com/containers/distro-bootc:version12", + imageID: "acf88e518194fac963a1b2e2e4110e38a4ce5fb3fceddd624fae8997d4566930", + buildImgref: "example.com/containers/distro-bootc:version12", sourceInfo: &osinfo.Info{ OSRelease: osinfo.OSRelease{ ID: "distroID", @@ -659,6 +662,24 @@ func NewTestBootcImageType(t *testing.T, imgTypeName string) *bootcImageType { return imgType.(*bootcImageType) } +func TestBootcImageTypeBootModeFromContainerType(t *testing.T) { + for _, tc := range []struct { + name string + containerType bootc.ContainerType + want platform.BootMode + }{ + {"traditional", "vmlinuz", platform.BOOT_HYBRID}, + {"android-aboot", bootc.ContainerTypeAboot, platform.BOOT_HYBRID}, + {"efi-aboot", bootc.ContainerTypeAbootEFI, platform.BOOT_UEFI}, + } { + t.Run(tc.name, func(t *testing.T) { + imageType := NewTestBootcImageType(t, "raw") + imageType.arch.distro.(*BootcDistro).containerType = tc.containerType + assert.Equal(t, tc.want, imageType.BootMode()) + }) + } +} + func getUserConfig() *blueprint.Blueprint { // add a user pass := randutil.String(20) diff --git a/pkg/image/bootc_disk.go b/pkg/image/bootc_disk.go index f2564111d6..360511f5f0 100644 --- a/pkg/image/bootc_disk.go +++ b/pkg/image/bootc_disk.go @@ -23,6 +23,7 @@ type BootcDiskImage struct { Bootloader *string UnifiedKernel bool + Aboot bool Partitions []PartitionConfig @@ -114,6 +115,7 @@ func (img *BootcDiskImage) InstantiateManifestFromContainers(m *manifest.Manifes } rawImage.Bootloader = img.Bootloader rawImage.UnifiedKernel = img.UnifiedKernel + rawImage.Aboot = img.Aboot rawImage.PartitionTable = img.PartitionTable rawImage.OSCustomizations = img.OSCustomizations rawImage.DiskCustomizations = img.DiskCustomizations diff --git a/pkg/manifest/raw_bootc.go b/pkg/manifest/raw_bootc.go index a156f32dc7..9c88bfcccf 100644 --- a/pkg/manifest/raw_bootc.go +++ b/pkg/manifest/raw_bootc.go @@ -37,6 +37,7 @@ type RawBootcImage struct { LiveBoot bool UnifiedKernel bool + Aboot bool Bootloader *string // customizations go here because there is no intermediate From ac6167f8fb858bb904f82ac50661fbc80d86e93e Mon Sep 17 00:00:00 2001 From: Alexander Larsson Date: Mon, 21 Sep 2026 10:08:46 +0200 Subject: [PATCH 2/2] bootc: Install aboot images with the composefs backend Aboot images contain their kernel, initramfs and command line in a partition payload, rather than a /boot filesystem layout. Select the composefs backend for aboot images, omit install-time kernel arguments, and skip post-install customizations that mount an OSTree deployment. Disable automatic XBOOTLDR creation and select UEFI boot when the image requests the ukiboot bootloader. For firmware-managed boot with bootloader=none, allow installation without an ESP. Relax both mount-generation and stage-validation checks while retaining the requirement for a root mount. Also, report errors if disk custimization is used with aboot, we don't currently support this. Note that aboot images still return true for unifiedKernel, but although there is a new boot_type field that can contain "uki", we use the old one to remain compatible with older bootc version that don't set boot_type. Add tests for composefs selection, omission of external kernel arguments, and manifest generation without an ESP. Partition layout and initial payload placement continue to come from disk.yaml. Assisted-by: AI Signed-off-by: Alexander Larsson --- pkg/distro/defs/bootc_imagetype.go | 45 +++++++++++++------ pkg/distro/defs/bootc_test.go | 28 ++++++++---- pkg/image/bootc_disk_test.go | 16 +++++++ pkg/manifest/raw_bootc.go | 22 ++++++--- pkg/manifest/raw_bootc_test.go | 21 +++++++++ .../bootc_install_to_filesystem_stage.go | 6 +-- .../bootc_install_to_filesystem_stage_test.go | 8 ++++ 7 files changed, 115 insertions(+), 31 deletions(-) diff --git a/pkg/distro/defs/bootc_imagetype.go b/pkg/distro/defs/bootc_imagetype.go index d5a5f83863..00d8eb0e94 100644 --- a/pkg/distro/defs/bootc_imagetype.go +++ b/pkg/distro/defs/bootc_imagetype.go @@ -11,6 +11,7 @@ import ( "github.com/osbuild/image-builder/internal/common" "github.com/osbuild/image-builder/pkg/arch" "github.com/osbuild/image-builder/pkg/bib/osinfo" + bootccontainer "github.com/osbuild/image-builder/pkg/bootc" "github.com/osbuild/image-builder/pkg/container" "github.com/osbuild/image-builder/pkg/customizations/anaconda" "github.com/osbuild/image-builder/pkg/customizations/ignition" @@ -94,17 +95,21 @@ func (t *bootcImageType) BasePartitionTable() (*disk.PartitionTable, error) { } func (t *bootcImageType) BootMode() platform.BootMode { - // We really never want HYBRID or LEGACY on aarch64 platforms. In the future - // it might be much nicer to take the same apporach as `Bootmode()` in the - // generic distro but that's a bit more involved. Let's start here. + bd := t.arch.distro.(*BootcDistro) + // Android aboot is firmware-managed, including on aarch64. The inspect + // API reports both Android and EFI aboot payloads as unified kernels. + if bd.containerType == bootccontainer.ContainerTypeAboot { + return platform.BOOT_NONE + } + + // Other aarch64 images use UEFI. if t.arch.arch == arch.ARCH_AARCH64 { return platform.BOOT_UEFI } - // Sealed images don't need a BIOSBOOT partition as they don't use bootupd - // (which requires it to exist), let's set ourselves to UEFI - bd := t.arch.distro.(*BootcDistro) - if bd.unifiedKernel { + // UKIs and EFI aboot payloads boot through UEFI without bootupd, + // so they do not need a BIOSBOOT partition. + if bd.unifiedKernel || bd.containerType.IsAbootEFI() { return platform.BOOT_UEFI } @@ -165,6 +170,9 @@ func (t *bootcImageType) SupportedBlueprintOptions() []string { // not configuration / customizations. These should always be implicitly // supported by all image types. bd := t.arch.distro.(*BootcDistro) + if bd.containerType.IsAboot() && t.Image == "bootc_disk" { + return []string{"name", "version", "description"} + } if bd.unifiedKernel && t.Image == "bootc_disk" { return []string{"customizations.disk", "name", "version", "description"} } @@ -917,6 +925,10 @@ func (t *bootcImageType) genPartitionTable(customizations *blueprint.Customizati if err != nil { return nil, fmt.Errorf("error reading disk customizations: %w", err) } + bd := t.arch.distro.(*BootcDistro) + if bd.containerType.IsAboot() && (bd.sourceInfo == nil || bd.sourceInfo.PartitionTable == nil) { + return nil, fmt.Errorf("aboot containers require an A/B partition table in disk.yaml") + } basept, err := t.basePartitionTable() if err != nil { return nil, err @@ -925,7 +937,7 @@ func (t *bootcImageType) genPartitionTable(customizations *blueprint.Customizati return nil, fmt.Errorf("pipelines: no partition tables defined for %s", t.arch.Name()) } - bd := t.arch.distro.(*BootcDistro) + isUKI := bd.unifiedKernel && !bd.containerType.IsAboot() // the user's own customizations, before the embedded ones are applied userBootCust := slices.ContainsFunc(fsCust, func(fs blueprint.FilesystemCustomization) bool { @@ -933,10 +945,9 @@ func (t *bootcImageType) genPartitionTable(customizations *blueprint.Customizati }) userDiskCust := diskCust != nil - // When there's a unified kernel we don't want to auto-create a /boot even *if* the - // root filesystem is btrfs or lvm. Set a policy that disables the creation. Otherwise - // the default partition table policy is used. - if bd.unifiedKernel { + // Unified kernels and aboot payloads do not use a separate /boot, even *if* + // the root filesystem is btrfs or lvm. Otherwise use the default policy. + if bd.unifiedKernel || bd.containerType.IsAboot() { // basept may be the table from the YAML definitions or the container, // work on a copy: the changes below modify its policy and partitions basept = basept.Clone().(*disk.PartitionTable) @@ -945,7 +956,7 @@ func (t *bootcImageType) genPartitionTable(customizations *blueprint.Customizati } basept.Policy.EnsureXBOOTLDR = false // A partition table that comes from the container is used as-is. - if bd.sourceInfo == nil || bd.sourceInfo.PartitionTable == nil { + if isUKI && (bd.sourceInfo == nil || bd.sourceInfo.PartitionTable == nil) { if err := setDPSRootPartitionType(basept, t.arch.arch); err != nil { return nil, err } @@ -969,6 +980,12 @@ func (t *bootcImageType) genPartitionTable(customizations *blueprint.Customizati } } + // Disk customizations replace the whole table and cannot describe the raw + // A/B payloads. Keep those partitions and their initial contents in disk.yaml. + if bd.containerType.IsAboot() && diskCust != nil { + return nil, fmt.Errorf("disk customizations are not supported for aboot containers: configure the A/B partition table in disk.yaml") + } + var partitionTable *disk.PartitionTable switch { case fsCust != nil && diskCust != nil: @@ -987,7 +1004,7 @@ func (t *bootcImageType) genPartitionTable(customizations *blueprint.Customizati // A disk customization builds a new partition table, so any /boot in it // was asked for by the user. - if bd.unifiedKernel && (userBootCust || (userDiskCust && partitionTable.FindMountable("/boot") != nil)) { + if isUKI && (userBootCust || (userDiskCust && partitionTable.FindMountable("/boot") != nil)) { return nil, fmt.Errorf("a /boot partition is not supported for bootc containers with a unified kernel (UKI): the UKI is installed into the ESP") } diff --git a/pkg/distro/defs/bootc_test.go b/pkg/distro/defs/bootc_test.go index d2573369db..2fe105de77 100644 --- a/pkg/distro/defs/bootc_test.go +++ b/pkg/distro/defs/bootc_test.go @@ -666,17 +666,29 @@ func TestBootcImageTypeBootModeFromContainerType(t *testing.T) { for _, tc := range []struct { name string containerType bootc.ContainerType + unifiedKernel bool want platform.BootMode }{ - {"traditional", "vmlinuz", platform.BOOT_HYBRID}, - {"android-aboot", bootc.ContainerTypeAboot, platform.BOOT_HYBRID}, - {"efi-aboot", bootc.ContainerTypeAbootEFI, platform.BOOT_UEFI}, + {"traditional", "vmlinuz", false, platform.BOOT_HYBRID}, + {"uki", "uki", true, platform.BOOT_UEFI}, + {"legacy-inspect-uki", "", true, platform.BOOT_UEFI}, + {"android-aboot", bootc.ContainerTypeAboot, true, platform.BOOT_NONE}, + {"efi-aboot", bootc.ContainerTypeAbootEFI, true, platform.BOOT_UEFI}, } { - t.Run(tc.name, func(t *testing.T) { - imageType := NewTestBootcImageType(t, "raw") - imageType.arch.distro.(*BootcDistro).containerType = tc.containerType - assert.Equal(t, tc.want, imageType.BootMode()) - }) + for _, architecture := range []arch.Arch{arch.ARCH_X86_64, arch.ARCH_AARCH64} { + t.Run(tc.name+"/"+architecture.String(), func(t *testing.T) { + imageType := NewTestBootcImageType(t, "raw") + imageType.arch.arch = architecture + bd := imageType.arch.distro.(*BootcDistro) + bd.containerType = tc.containerType + bd.unifiedKernel = tc.unifiedKernel + want := tc.want + if architecture == arch.ARCH_AARCH64 && tc.containerType == "vmlinuz" { + want = platform.BOOT_UEFI + } + assert.Equal(t, want, imageType.BootMode()) + }) + } } } diff --git a/pkg/image/bootc_disk_test.go b/pkg/image/bootc_disk_test.go index b51426cadc..e5f0205048 100644 --- a/pkg/image/bootc_disk_test.go +++ b/pkg/image/bootc_disk_test.go @@ -48,6 +48,7 @@ type bootcDiskImageTestOpts struct { Files []*fsnode.File KernelOptionsAppend []string + Aboot bool } func makeFakePlatform(opts *bootcDiskImageTestOpts) platform.Platform { @@ -83,6 +84,7 @@ func makeBootcDiskImageOsbuildManifest(t *testing.T, opts *bootcDiskImageTestOpt img.OSCustomizations.SELinux = opts.SELinux img.OSCustomizations.Files = opts.Files img.OSCustomizations.Directories = opts.Directories + img.Aboot = opts.Aboot m := &manifest.Manifest{} runi := &runner.Fedora{} @@ -181,6 +183,20 @@ func TestBootcDiskImageUsesBootcInstallToFs(t *testing.T) { assert.Equal(t, []interface{}{"karg1", "karg2"}, bootcOpts["kernel-args"]) } +func TestBootcDiskImageAbootUsesComposefs(t *testing.T) { + opts := &bootcDiskImageTestOpts{ + Aboot: true, + KernelOptionsAppend: []string{"karg1", "karg2"}, + } + osbuildManifest := makeBootcDiskImageOsbuildManifest(t, opts) + + imagePipeline := findPipelineFromOsbuildManifest(t, osbuildManifest, "image") + bootcStage := findStageFromOsbuildPipeline(t, imagePipeline, "org.osbuild.bootc.install-to-filesystem") + bootcOpts := bootcStage["options"].(map[string]interface{}) + assert.Equal(t, true, bootcOpts["composefs"]) + assert.NotContains(t, bootcOpts, "kernel-args") +} + func TestBootcDiskImageExportPipelines(t *testing.T) { require := require.New(t) diff --git a/pkg/manifest/raw_bootc.go b/pkg/manifest/raw_bootc.go index 9c88bfcccf..86e7170a67 100644 --- a/pkg/manifest/raw_bootc.go +++ b/pkg/manifest/raw_bootc.go @@ -62,6 +62,15 @@ func (p *RawBootcImage) SetFilename(filename string) { p.filename = filename } +func (p *RawBootcImage) installPlatform() platform.Platform { + // Aboot installs do not use bootupd or require the ESP to be mounted at + // /boot/efi. Bootc discovers and mounts an ESP itself for ukiboot. + if p.Aboot { + return &platform.Data{} + } + return p.platform +} + func NewRawBootcImage(buildPipeline Build, containers []container.SourceSpec, platform platform.Platform) *RawBootcImage { p := &RawBootcImage{ Base: NewBase("image", buildPipeline), @@ -167,11 +176,11 @@ func (p *RawBootcImage) serialize() (osbuild.Pipeline, error) { } opts := &osbuild.BootcInstallToFilesystemOptions{} // Unified kernels cannot have custom kernel options - if !p.UnifiedKernel { + if !p.UnifiedKernel && !p.Aboot { opts.Kargs = p.OSCustomizations.KernelOptionsAppend } // Unified implies that the composefs backend must be used - if p.UnifiedKernel { + if p.UnifiedKernel || p.Aboot { opts.ComposeFS = common.ToPtr(true) } if p.Bootloader != nil { @@ -183,11 +192,12 @@ func (p *RawBootcImage) serialize() (osbuild.Pipeline, error) { inputs := osbuild.ContainerDeployInputs{ Images: osbuild.NewContainersInputForSingleSource(p.containerSpecs[0]), } - devices, mounts, err := osbuild.GenBootupdDevicesMounts(p.filename, p.PartitionTable, p.platform) + installPlatform := p.installPlatform() + devices, mounts, err := osbuild.GenBootupdDevicesMounts(p.filename, p.PartitionTable, installPlatform) if err != nil { return osbuild.Pipeline{}, err } - st, err := osbuild.NewBootcInstallToFilesystemStage(opts, inputs, devices, mounts, p.platform) + st, err := osbuild.NewBootcInstallToFilesystemStage(opts, inputs, devices, mounts, installPlatform) if err != nil { return osbuild.Pipeline{}, err } @@ -197,7 +207,7 @@ func (p *RawBootcImage) serialize() (osbuild.Pipeline, error) { pipeline.AddStage(stage) } - if !p.UnifiedKernel { + if !p.UnifiedKernel && !p.Aboot { // all our customizations work directly on the mounted deployment // root from the image so generate the devices/mounts for all devices, mounts, err = osbuild.GenBootupdDevicesMounts(p.filename, p.PartitionTable, p.platform) @@ -422,7 +432,7 @@ func (p *RawBootcImage) serialize() (osbuild.Pipeline, error) { func (p *RawBootcImage) genMountpointSELinuxStages() ([]*osbuild.Stage, error) { stages := make([]*osbuild.Stage, 0, 3) - devices, allMounts, err := osbuild.GenBootupdDevicesMounts(p.filename, p.PartitionTable, p.platform) + devices, allMounts, err := osbuild.GenBootupdDevicesMounts(p.filename, p.PartitionTable, p.installPlatform()) if err != nil { return nil, fmt.Errorf("generating devices/mounts for mountpoint SELinux labeling: %w", err) } diff --git a/pkg/manifest/raw_bootc_test.go b/pkg/manifest/raw_bootc_test.go index 6268af7d55..532b92b3c2 100644 --- a/pkg/manifest/raw_bootc_test.go +++ b/pkg/manifest/raw_bootc_test.go @@ -100,6 +100,27 @@ func TestRawBootcImageSerializeMountsValidated(t *testing.T) { assert.EqualError(t, err, `required mounts for bootupd stage [/boot/efi] missing`) } +func TestRawBootcImageAbootWithoutESP(t *testing.T) { + pipeline := makeFakeRawBootcPipeline() + pipeline.Aboot = true + pipeline.PartitionTable = testdisk.MakeFakePartitionTable("/") + result, err := pipeline.Serialize() + require.NoError(t, err) + stage := findStage("org.osbuild.bootc.install-to-filesystem", result.Stages) + require.NotNil(t, stage) + opts := stage.Options.(*osbuild.BootcInstallToFilesystemOptions) + assert.Equal(t, common.ToPtr(true), opts.ComposeFS) + assert.Empty(t, opts.Bootloader) +} + +func TestRawBootcImageNonAbootNoneStillNeedsESP(t *testing.T) { + pipeline := makeFakeRawBootcPipeline() + pipeline.Bootloader = common.ToPtr("none") + pipeline.PartitionTable = testdisk.MakeFakePartitionTable("/") + _, err := pipeline.Serialize() + require.EqualError(t, err, "required mounts for bootupd stage [/boot/efi] missing") +} + func findMountIdx(mounts []osbuild.Mount, mntType string) int { for i, mnt := range mounts { if mnt.Type == mntType { diff --git a/pkg/osbuild/bootc_install_to_filesystem_stage.go b/pkg/osbuild/bootc_install_to_filesystem_stage.go index 0275b6bcf8..7a32fbeae0 100644 --- a/pkg/osbuild/bootc_install_to_filesystem_stage.go +++ b/pkg/osbuild/bootc_install_to_filesystem_stage.go @@ -27,9 +27,9 @@ func (BootcInstallToFilesystemOptions) isStageOptions() {} // NewBootcInstallToFilesystem creates a new stage for the // org.osbuild.bootc.install-to-filesystem stage. // -// It requires a mount setup so that bootupd can be run by bootc. I.e -// "/", "/boot" and "/boot/efi" need to be set up so that -// bootc/bootupd find and install all required bootloader bits. +// It requires a root mount, and on UEFI platforms also an ESP mount so +// bootupd can run. Aboot callers pass a platform without a UEFI vendor: +// bootc discovers and mounts an ESP itself for ukiboot. // // The mounts input should be generated with GenBootupdDevicesMounts. func NewBootcInstallToFilesystemStage(options *BootcInstallToFilesystemOptions, inputs ContainerDeployInputs, devices map[string]Device, mounts []Mount, pltf platform.Platform) (*Stage, error) { diff --git a/pkg/osbuild/bootc_install_to_filesystem_stage_test.go b/pkg/osbuild/bootc_install_to_filesystem_stage_test.go index ad52834fc3..27de4ee2c8 100644 --- a/pkg/osbuild/bootc_install_to_filesystem_stage_test.go +++ b/pkg/osbuild/bootc_install_to_filesystem_stage_test.go @@ -47,6 +47,14 @@ func TestBootcInstallToFilesystemStageNewHappy(t *testing.T) { assert.Equal(t, stage, expectedStage) } +func TestBootcInstallToFilesystemStageWithoutUEFIVendor(t *testing.T) { + pf := &platform.Data{Arch: arch.ARCH_X86_64} + _, err := osbuild.NewBootcInstallToFilesystemStage(nil, makeFakeContainerInputs(), nil, makeOsbuildMounts("/"), pf) + require.NoError(t, err) + _, err = osbuild.NewBootcInstallToFilesystemStage(nil, makeFakeContainerInputs(), nil, nil, pf) + require.ErrorContains(t, err, "required mounts for bootupd stage [/] missing") +} + func TestBootcInstallToFilesystemStageNewEssentialMountsOnly(t *testing.T) { devices := makeOsbuildDevices("dev-for-/", "dev-for-/boot/efi", "dev-for-/var/log") mounts := makeOsbuildMounts("/", "/boot/efi", "/var/log")