Skip to content
Merged
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
21 changes: 21 additions & 0 deletions pkg/bootc/bootc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
25 changes: 19 additions & 6 deletions pkg/bootc/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
Expand All @@ -394,24 +406,25 @@ 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"`
} `json:"kernel"`
}

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) {
Expand Down
18 changes: 18 additions & 0 deletions pkg/bootc/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions pkg/distro/defs/bootc.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type BootcDistro struct {
sourceInfo *osinfo.Info
buildSourceInfo *osinfo.Info
unifiedKernel bool
containerType bootc.ContainerType
bootloader *string

id distro.ID
Expand Down Expand Up @@ -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
Expand Down
46 changes: 32 additions & 14 deletions pkg/distro/defs/bootc_imagetype.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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"}
}
Expand Down Expand Up @@ -266,6 +274,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())
Expand Down Expand Up @@ -916,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
Expand All @@ -924,18 +937,17 @@ 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 {
return fs.Mountpoint == "/boot"
})
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)
Expand All @@ -944,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
}
Expand All @@ -968,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:
Expand All @@ -986,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")
}

Expand Down
39 changes: 36 additions & 3 deletions pkg/distro/defs/bootc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -659,6 +662,36 @@ 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
unifiedKernel bool
want platform.BootMode
}{
{"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},
} {
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())
})
}
}
}

func getUserConfig() *blueprint.Blueprint {
// add a user
pass := randutil.String(20)
Expand Down
2 changes: 2 additions & 0 deletions pkg/image/bootc_disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type BootcDiskImage struct {

Bootloader *string
UnifiedKernel bool
Aboot bool

Partitions []PartitionConfig

Expand Down Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions pkg/image/bootc_disk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ type bootcDiskImageTestOpts struct {
Files []*fsnode.File

KernelOptionsAppend []string
Aboot bool
}

func makeFakePlatform(opts *bootcDiskImageTestOpts) platform.Platform {
Expand Down Expand Up @@ -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{}
Expand Down Expand Up @@ -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)

Expand Down
Loading
Loading