Skip to content
Closed
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
> [!IMPORTANT]
> Remove this line to confirm you've reviewed this PR before submitting.
# image-builder CLI

Build images from the command line in a convenient way.
Expand Down
5 changes: 5 additions & 0 deletions pkg/bootc/bootc.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,9 @@ type Info struct {

// What bootloader should be passed?
Bootloader *string

// Is the container built for the composefs backend (it ships
// setup-root-conf.toml and no ostree prepare-root.conf)? A unified
// kernel implies the composefs backend regardless of this setting.
ComposefsBackend bool
}
2 changes: 2 additions & 0 deletions pkg/bootc/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ package bootc
func (cnt *Container) ID() string {
return cnt.id
}

var ComposefsBackend = composefsBackend
48 changes: 48 additions & 0 deletions pkg/bootc/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,12 @@ func (c *Container) ResolveInfo(variant string) (*Info, error) {
bootcInfo.DefaultRootFs = bootcInstallConfig.Filesystem.Root.Type
bootcInfo.Bootloader = bootcInstallConfig.Bootloader

composefs, err := composefsBackend(c.RootFS())
if err != nil {
return nil, err
}
bootcInfo.ComposefsBackend = composefs

unifiedKernel, err := c.UnifiedKernel()
if err != nil {
return nil, err
Expand Down Expand Up @@ -382,6 +388,48 @@ func (c *Container) InitrdModules(kver string) ([]string, error) {
return strings.Split(strings.TrimRight(string(output), "\n"), "\n"), nil
}

// setupRootConfPath is the configuration of bootc's composefs root setup.
// bootc treats a container that ships it, even empty, as built for the
// composefs backend, see bootc-setup-root-conf.toml(5).
const setupRootConfPath = "usr/lib/composefs/setup-root-conf.toml"

// ostreePrepareRootConfPaths are the locations of ostree's
// prepare-root.conf, without which a container can't be installed with the
// ostree backend.
var ostreePrepareRootConfPaths = []string{
"etc/ostree/prepare-root.conf",
"usr/lib/ostree/prepare-root.conf",
}

// composefsBackend finds out if bootc installs the container with the
// composefs backend even without a unified kernel: it does so when the
// container has a setup-root-conf.toml and no ostree prepare-root.conf, see
// "The storage backend" in bootc-installation(7).
func composefsBackend(root fs.FS) (bool, error) {
native, err := fileExists(root, setupRootConfPath)
if err != nil || !native {
return false, err
}
for _, path := range ostreePrepareRootConfPaths {
hasOstree, err := fileExists(root, path)
if err != nil || hasOstree {
return false, err
}
}
return true, nil
}

func fileExists(root fs.FS, name string) (bool, error) {
_, err := fs.Stat(root, name)
if errors.Is(err, fs.ErrNotExist) {
return false, nil
}
if err != nil {
return false, fmt.Errorf("checking for /%s in the container failed: %w", name, err)
}
return true, nil
}

// UnifiedKernel finds out if the kernel inside the bootc container is unified
func (c *Container) UnifiedKernel() (bool, error) {
args := []string{"exec"}
Expand Down
27 changes: 27 additions & 0 deletions pkg/bootc/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"path"
"path/filepath"
"testing"
"testing/fstest"

"github.com/osbuild/image-builder/internal/common"
"github.com/osbuild/image-builder/pkg/bootc"
Expand Down Expand Up @@ -282,3 +283,29 @@ echo '%s'
assert.Equal(t, tc.Out, installConfig.Bootloader)
}
}

func TestComposefsBackend(t *testing.T) {
const setupRoot = "usr/lib/composefs/setup-root-conf.toml"
const ostreeConf = "usr/lib/ostree/prepare-root.conf"
const ostreeConfEtc = "etc/ostree/prepare-root.conf"
for _, tc := range []struct {
files []string
expected bool
}{
{nil, false},
{[]string{setupRoot}, true},
{[]string{ostreeConf}, false},
{[]string{ostreeConfEtc}, false},
{[]string{setupRoot, ostreeConf}, false},
{[]string{setupRoot, ostreeConfEtc}, false},
} {
root := fstest.MapFS{}
for _, f := range tc.files {
// both files count even when empty
root[f] = &fstest.MapFile{}
}
composefs, err := bootc.ComposefsBackend(root)
require.NoError(t, err)
assert.Equal(t, tc.expected, composefs, tc.files)
}
}
6 changes: 5 additions & 1 deletion pkg/distro/defs/bootc.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ type BootcDistro struct {
sourceInfo *osinfo.Info
buildSourceInfo *osinfo.Info
unifiedKernel bool
bootloader *string
// the container is built for the composefs backend (a unified kernel
// implies it as well)
composefs bool
bootloader *string

id distro.ID
defaultFs string
Expand Down Expand Up @@ -140,6 +143,7 @@ func NewBootcWithLoader(loader *Loader, name string, cinfo *bootc.Info) (*BootcD
rootfsMinSize: cinfo.Size * containerSizeToDiskSizeMultiplier,
bootloader: cinfo.Bootloader,
unifiedKernel: cinfo.UnifiedKernel,
composefs: cinfo.ComposefsBackend,
}

// load image types from bootc-generic-1
Expand Down
130 changes: 130 additions & 0 deletions pkg/distro/defs/bootc_image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package defs

import (
"math/rand"
"slices"
"strings"
"testing"

"github.com/osbuild/blueprint/pkg/blueprint"
Expand All @@ -12,6 +14,7 @@ import (
"github.com/osbuild/image-builder/pkg/disk/partition"
"github.com/osbuild/image-builder/pkg/distro"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func createRand() *rand.Rand {
Expand Down Expand Up @@ -722,3 +725,130 @@ func TestManifestSubscriptionCustomization(t *testing.T) {
})
}
}

func TestManifestPXEComposefsUnsupported(t *testing.T) {
imgType := NewTestBootcImageType(t, "pxe-tar-xz")
_, _, err := imgType.Manifest(&blueprint.Blueprint{}, distro.ImageOptions{}, nil, common.ToPtr(int64(0)))
require.NoError(t, err)

imgType.arch.distro.(*BootcDistro).composefs = true
_, _, err = imgType.Manifest(&blueprint.Blueprint{}, distro.ImageOptions{}, nil, common.ToPtr(int64(0)))
assert.EqualError(t, err, `image type "pxe-tar-xz" is not supported for bootc containers built for the composefs backend`)
}

func TestManifestComposefsCustomizationsWarn(t *testing.T) {
const ukiWarnPrefix = `blueprint validation failed for image type "qcow2": the bootc container has a unified kernel (UKI), which does not support: `
const composefsWarnPrefix = `blueprint validation failed for image type "qcow2": the bootc container is built for the composefs backend, which does not support: `
rootPart := blueprint.PartitionCustomization{
Type: "plain",
FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{Mountpoint: "/", FSType: "ext4"},
}
dataPart := blueprint.PartitionCustomization{
Type: "plain",
MinSize: datasizes.GiB,
FilesystemTypedCustomization: blueprint.FilesystemTypedCustomization{Mountpoint: "/var/data", FSType: "ext4"},
}

for name, tc := range map[string]struct {
customizations *blueprint.Customizations
options distro.ImageOptions
expected string
// only a unified kernel does not support it
ukiOnly bool
}{
"empty": {},
"disk-root-only": {
customizations: &blueprint.Customizations{
Disk: &blueprint.DiskCustomization{Partitions: []blueprint.PartitionCustomization{rootPart}},
},
},
"user": {
customizations: &blueprint.Customizations{User: []blueprint.UserCustomization{{Name: "alice"}}},
expected: "customizations.user",
},
"kargs": {
customizations: &blueprint.Customizations{Kernel: &blueprint.KernelCustomization{Append: "debug"}},
expected: "customizations.kernel.append",
ukiOnly: true,
},
"group": {
customizations: &blueprint.Customizations{Group: []blueprint.GroupCustomization{{Name: "wheel2"}}},
expected: "customizations.group",
},
"files-and-dirs": {
customizations: &blueprint.Customizations{
Directories: []blueprint.DirectoryCustomization{{Path: "/etc/foo"}},
Files: []blueprint.FileCustomization{{Path: "/etc/foo/bar", Data: "baz"}},
},
expected: "customizations.directories, customizations.files",
},
"ignition": {
customizations: &blueprint.Customizations{
Ignition: &blueprint.IgnitionCustomization{FirstBoot: &blueprint.FirstBootIgnitionCustomization{ProvisioningURL: "https://example.com/config.ign"}},
},
expected: "customizations.ignition",
},
"bootloader": {
customizations: getBootloaderConfig().Customizations,
expected: "customizations.bootloader",
},
"subscription": {
options: distro.ImageOptions{
Subscription: &subscription.ImageOptions{Organization: "2040324", ActivationKey: "my-secret-key"},
},
expected: "subscription",
},
"disk-extra-mountpoint": {
customizations: &blueprint.Customizations{
Disk: &blueprint.DiskCustomization{Partitions: []blueprint.PartitionCustomization{rootPart, dataPart}},
},
expected: "customizations.disk mountpoints without an fstab (/var/data)",
},
"filesystem-extra-mountpoint": {
customizations: &blueprint.Customizations{
Filesystem: []blueprint.FilesystemCustomization{{Mountpoint: "/", MinSize: datasizes.GiB}, {Mountpoint: "/var/data", MinSize: datasizes.GiB}},
},
expected: "customizations.filesystem mountpoints without an fstab (/var/data)",
},
} {
t.Run(name, func(t *testing.T) {
imgType := NewTestBootcImageType(t, "qcow2")
bp := &blueprint.Blueprint{Customizations: tc.customizations}

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

// only look at the composefs warnings, e.g.
// customizations.filesystem is not a supported option for
// bootc disks in the first place
composefsWarnings := func() []string {
_, warnings, err := imgType.Manifest(bp, tc.options, nil, common.ToPtr(int64(0)))
require.NoError(t, err)
return slices.DeleteFunc(warnings, func(w string) bool {
return !strings.HasPrefix(w, ukiWarnPrefix) && !strings.HasPrefix(w, composefsWarnPrefix)
})
}

// with ostree all of these are applied
assert.Empty(t, composefsWarnings())

bd.composefs = true
if tc.expected == "" || tc.ukiOnly {
assert.Empty(t, composefsWarnings())
} else {
assert.Equal(t, []string{composefsWarnPrefix + tc.expected}, composefsWarnings())
}

// a unified kernel implies composefs, whether or not the
// container is built for it
for _, composefs := range []bool{true, false} {
bd.composefs = composefs
bd.unifiedKernel = true
if tc.expected == "" {
assert.Empty(t, composefsWarnings())
} else {
assert.Equal(t, []string{ukiWarnPrefix + tc.expected}, composefsWarnings())
}
}
})
}
}
Loading