Skip to content
2 changes: 1 addition & 1 deletion data/dependencies/deps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ import (
)

func TestMinimumOSBuildVersion(t *testing.T) {
assert.Equal(t, "178", dependencies.MinimumOSBuildVersion())
assert.Equal(t, "180", dependencies.MinimumOSBuildVersion())
}
2 changes: 1 addition & 1 deletion data/dependencies/osbuild
Original file line number Diff line number Diff line change
@@ -1 +1 @@
178
180
6 changes: 6 additions & 0 deletions pkg/bootc/bootc.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,10 @@ type Info struct {

// The size of the container image
Size uint64

// Is the container using a unified kernel?
UnifiedKernel bool

// What bootloader should be passed?
Bootloader *string
}
85 changes: 62 additions & 23 deletions pkg/bootc/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,18 @@ func (c *Container) ResolveInfo() (*Info, error) {
}
bootcInfo.OSInfo = os

defaultFs, err := c.DefaultRootfsType()
bootcInstallConfig, err := c.InstallConfiguration()
if err != nil {
return nil, err
}
bootcInfo.DefaultRootFs = defaultFs
bootcInfo.DefaultRootFs = bootcInstallConfig.Filesystem.Root.Type
bootcInfo.Bootloader = bootcInstallConfig.Bootloader

unifiedKernel, err := c.UnifiedKernel()
if err != nil {
return nil, err
}
bootcInfo.UnifiedKernel = unifiedKernel

size, err := getContainerSize(c.ref, c.extraOpts)
if err != nil {
Expand Down Expand Up @@ -267,51 +274,56 @@ func (c *Container) ExecArgv() []string {
return args
}

// DefaultRootfsType returns the default rootfs type (e.g. "ext4") as
// specified by the bootc container install configuration. An empty
// string is valid and means the container sets no default.
func (c *Container) DefaultRootfsType() (string, error) {
type BootcInstallConfiguration struct {
Filesystem struct {
Root struct {
Type string `json:"type"`
} `json:"root"`
} `json:"filesystem"`

Bootloader *string `json:"bootloader"`
}

// InstallConfiguration returns the install configuration for bootc container
// as given by `bootc install print-configuration`
func (c *Container) InstallConfiguration() (BootcInstallConfiguration, error) {
args := []string{"exec"}
args = append(args, c.extraOpts...)
args = append(args, c.id, "bootc", "install", "print-configuration")

/* #nosec G204 */
output, err := exec.Command("podman", args...).Output()
if err != nil {
return "", fmt.Errorf("failed to run bootc install print-configuration: %w, output:\n%s", err, output)
return BootcInstallConfiguration{}, fmt.Errorf("failed to run bootc install print-configuration: %w, output:\n%s", err, output)
}

var bootcConfig struct {
Filesystem struct {
Root struct {
Type string `json:"type"`
} `json:"root"`
} `json:"filesystem"`
}
var bootcInstallConfig BootcInstallConfiguration

if err := json.Unmarshal(output, &bootcConfig); err != nil {
return "", fmt.Errorf("failed to unmarshal bootc configuration: %w", err)
if err := json.Unmarshal(output, &bootcInstallConfig); err != nil {
return BootcInstallConfiguration{}, fmt.Errorf("failed to unmarshal bootc install configuration: %w", err)
}

// filesystem.root.type is the preferred way instead of the old root-fs-type top-level key.
// See https://github.com/containers/bootc/commit/558cd4b1d242467e0ffec77fb02b35166469dcc7
fsType := bootcConfig.Filesystem.Root.Type
fsType := bootcInstallConfig.Filesystem.Root.Type

// Return early to skip validating the empty default root filesystem type
if fsType == "" {
return bootcInstallConfig, nil
}

// Note that these are the only filesystems that the "images" library
// knows how to handle, i.e. how to construct the required osbuild
// stages for.
// TODO: move this into a helper in "images" so that there is only
// a single place that needs updating when we add e.g. btrfs or
// bcachefs
supportedFS := []string{"ext4", "xfs", "btrfs"}

if fsType == "" {
return "", nil
}
if !slices.Contains(supportedFS, fsType) {
return "", fmt.Errorf("unsupported root filesystem type: %s, supported: %s", fsType, strings.Join(supportedFS, ", "))
return BootcInstallConfiguration{}, fmt.Errorf("unsupported root filesystem type: %s, supported: %s", fsType, strings.Join(supportedFS, ", "))
}

return fsType, nil
return bootcInstallConfig, nil
}

// InitrdModules gets the list of modules from the container's initrd
Expand All @@ -332,6 +344,33 @@ func (c *Container) InitrdModules(kver string) ([]string, error) {
return strings.Split(strings.TrimRight(string(output), "\n"), "\n"), nil
}

// UnifiedKernel finds out if the kernel inside the bootc container is unified
func (c *Container) UnifiedKernel() (bool, error) {
args := []string{"exec"}
args = append(args, c.extraOpts...)
args = append(args, c.id, "bootc", "container", "inspect", "--json")

/* #nosec G204 */
output, err := exec.Command("podman", args...).Output()
if err != nil {
return false, fmt.Errorf("failed to run bootc container inspect: %w, output:\n%s", err, output)
}

var bootcInspect struct {
Kargs []string `json:"kargs"`
Kernel struct {
Version string `json:"version"`
Unified bool `json:"unified"`
} `json:"kernel"`
Comment on lines +354 to +358
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: the kargs and version don't seem to be used anywhere or even unit-tested. I think that it is useful to unmarshal all this information while we are at it, but technically this is a dead code, because on the kernel.unified value is ever returned from this method.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea this is true; I had some plans to start using them. I'll try to do that in a follow-up. For example we have our own kernel-scanning code at the moment for the version and now that bootc can give it we might be able to move some bits around.

}

if err := json.Unmarshal(output, &bootcInspect); err != nil {
return false, fmt.Errorf("failed to unmarshal bootc inspect : %w", err)
}

return bootcInspect.Kernel.Unified, nil
}

func findImageIdFor(cntId, ref string, extraOpts []string) (string, error) {
args := []string{"inspect"}
args = append(args, extraOpts...)
Expand Down
51 changes: 48 additions & 3 deletions pkg/bootc/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"path/filepath"
"testing"

"github.com/osbuild/images/internal/common"
"github.com/osbuild/images/pkg/bootc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -205,9 +206,9 @@ func TestRootfsTypeHappy(t *testing.T) {
echo '%s'
`, jsonStr))
cnt := bootc.Container{}
rootfs, err := cnt.DefaultRootfsType()
installConfig, err := cnt.InstallConfiguration()
assert.NoError(t, err)
assert.Equal(t, tc, rootfs)
assert.Equal(t, tc, installConfig.Filesystem.Root.Type)
}
}

Expand All @@ -218,7 +219,51 @@ func TestRootfsTypeSad(t *testing.T) {
echo '%s'
`, jsonStr))
cnt := bootc.Container{}
_, err := cnt.DefaultRootfsType()
_, err := cnt.InstallConfiguration()
assert.ErrorContains(t, err, "unsupported root filesystem type: ext1, supported: ")
}
}

func TestUnifiedKernelHappy(t *testing.T) {
for _, tc := range []struct {
In string
Out bool
}{
{`{"kernel": {"unified": true}}`, true},
{`{"kernel": {"unified": false}}`, false},
{`{"kernel": {}}`, false},
{`{}`, false},
} {
makeFakePodman(t, fmt.Sprintf(`#!/bin/sh
echo '%s'
`, tc.In))
cnt := bootc.Container{}
unified, err := cnt.UnifiedKernel()
assert.NoError(t, err)
assert.Equal(t, tc.Out, unified)
}
}

func TestBootloaderHappy(t *testing.T) {
for _, tc := range []struct {
In string
Out *string
}{
{"", nil},
{"systemd", common.ToPtr("systemd")},
{"none", common.ToPtr("none")},
{"grub", common.ToPtr("grub")},
} {
jsonStr := "{}"
if tc.In != "" {
jsonStr = fmt.Sprintf(`{"bootloader": "%s"}`, tc.In)
}
makeFakePodman(t, fmt.Sprintf(`#!/bin/sh
echo '%s'
`, jsonStr))
cnt := bootc.Container{}
installConfig, err := cnt.InstallConfiguration()
assert.NoError(t, err)
assert.Equal(t, tc.Out, installConfig.Bootloader)
}
}
13 changes: 8 additions & 5 deletions pkg/distro/bootc/bootctest/exe/bootc.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ import (
)

func fakeBootc() error {
if os.Args[1] != "install" || os.Args[2] != "print-configuration" {
return fmt.Errorf("unexpected bootc arguments %v", os.Args)
if len(os.Args) >= 3 && os.Args[1] == "install" && os.Args[2] == "print-configuration" {
fmt.Println(`{"filesystem": {"root": {"type": "ext4"}}}`)
return nil
}
// print a sensible default configuration
fmt.Println(`{"filesystem": {"root": {"type": "ext4"}}}`)
return nil
if len(os.Args) >= 4 && os.Args[1] == "container" && os.Args[2] == "inspect" && os.Args[3] == "--json" {
fmt.Println(`{"kernel": {"unified": false}}`)
return nil
}
return fmt.Errorf("unexpected bootc arguments %v", os.Args)
}

func fakeSleep() error {
Expand Down
4 changes: 4 additions & 0 deletions pkg/distro/generic/bootc.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ type BootcDistro struct {
buildImageID string
sourceInfo *osinfo.Info
buildSourceInfo *osinfo.Info
unifiedKernel bool
bootloader *string

id distro.ID
defaultFs string
Expand Down Expand Up @@ -124,6 +126,8 @@ func NewBootc(name string, cinfo *bootc.Info) (*BootcDistro, error) {
defaultFs: cinfo.DefaultRootFs,
releasever: osInfo.OSRelease.VersionID,
rootfsMinSize: cinfo.Size * containerSizeToDiskSizeMultiplier,
bootloader: cinfo.Bootloader,
unifiedKernel: cinfo.UnifiedKernel,
}

// load image types from bootc-generic-1
Expand Down
20 changes: 20 additions & 0 deletions pkg/distro/generic/bootc_imagetype.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,13 @@ func (t *bootcImageType) BootMode() platform.BootMode {
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 {
return platform.BOOT_UEFI
}

return platform.BOOT_HYBRID
}

Expand Down Expand Up @@ -210,6 +217,10 @@ func (t *bootcImageType) manifestForDisk(bp *blueprint.Blueprint, options distro
if opts := buildOptions(t); opts != nil {
img.BuildOptions = opts
}

img.Bootloader = bd.bootloader
img.UnifiedKernel = bd.unifiedKernel

img.OSCustomizations.Users = users.UsersFromBP(customizations.GetUsers())

groups, err := customizations.GetGroups()
Expand Down Expand Up @@ -779,6 +790,15 @@ func (t *bootcImageType) genPartitionTable(customizations *blueprint.Customizati

bd := t.arch.distro.(*BootcDistro)

// 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 {
basept.Policy = &disk.PartitionTablePolicy{
EnsureXBOOTLDR: false,
}
}

// Embedded disk customization applies if there was no local customization
if fsCust == nil && diskCust == nil && bd.sourceInfo != nil && bd.sourceInfo.ImageCustomization != nil {
imageCustomizations := bd.sourceInfo.ImageCustomization
Expand Down
5 changes: 5 additions & 0 deletions pkg/image/bootc_disk.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ type BootcDiskImage struct {
ContainerSource *container.SourceSpec
BuildContainerSource *container.SourceSpec

Bootloader *string
UnifiedKernel bool

// Customizations
OSCustomizations manifest.OSCustomizations
DiskCustomizations manifest.DiskCustomizations
Expand Down Expand Up @@ -107,6 +110,8 @@ func (img *BootcDiskImage) InstantiateManifestFromContainers(m *manifest.Manifes
if customSourcePipeline != "" {
rawImage.SourcePipeline = customSourcePipeline
}
rawImage.Bootloader = img.Bootloader
rawImage.UnifiedKernel = img.UnifiedKernel
rawImage.PartitionTable = img.PartitionTable
rawImage.OSCustomizations = img.OSCustomizations
rawImage.DiskCustomizations = img.DiskCustomizations
Expand Down
Loading
Loading