Skip to content
Draft
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Interesting. osbuild is trying to do with this what bootc is doing with https://github.com/bootc-dev/bootc/blob/main/AGENTS.md#signed-off-by

There's advantages/disadvantages to both...but the thing I like is that it's straightforward to gate on the DCO in CI and it basically exists for a related purpose

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

IOW I did review this and you can review the marker befor epushing

# 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

// Does the container's bootc have "bootc install mount"? Its
// deployment can then be mounted with the org.osbuild.bootc.deployment
// mount, whatever the storage backend, when it is the build root.
InstallMount bool
}
45 changes: 42 additions & 3 deletions pkg/bootc/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,12 @@ func (c *Container) ResolveInfo(variant string) (*Info, error) {
}
bootcInfo.UnifiedKernel = unifiedKernel

installMount, err := c.HasInstallMount()
if err != nil {
return nil, err
}
bootcInfo.InstallMount = installMount

size, err := getContainerSize(c.ref, c.storeOpts)
if err != nil {
return nil, err
Expand All @@ -254,10 +260,17 @@ func (c *Container) ResolveInfo(variant string) (*Info, error) {
// used for build containers where we don't need all the information and trying
// to get it might break things.
func (c *Container) ResolveBuildInfo() (*Info, error) {
// The build container's bootc is the one that installs and mounts
// the deployment. Not all image types run bootc from the build
// container, so one without a working bootc is not an error here;
// the deployment is then mounted the way it was before "install
// mount".
installMount, _ := c.HasInstallMount()
return &Info{
Imgref: c.ref,
ImageID: c.id,
Arch: c.Arch(),
Imgref: c.ref,
ImageID: c.id,
Arch: c.Arch(),
InstallMount: installMount,
}, nil
}

Expand Down Expand Up @@ -414,6 +427,32 @@ func (c *Container) UnifiedKernel() (bool, error) {
return bootcInspect.Kernel.Unified, nil
}

// clapUsageExitCode is the exit code of bootc (from its clap argument
// parser) for an unknown command.
const clapUsageExitCode = 2

// HasInstallMount finds out if the container's bootc has the "bootc install
// mount" command (bootc-dev/bootc#2483). The command is probed for rather
// than the bootc version, which doesn't tell for builds from git or
// backports.
func (c *Container) HasInstallMount() (bool, error) {
args := []string{"exec"}
args = append(args, c.storeOpts...)
args = append(args, c.id, "bootc", "install", "mount", "--help")

/* #nosec G204 */
if _, err := exec.Command("podman", args...).Output(); err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
if exitErr.ExitCode() == clapUsageExitCode {
return false, nil
}
return false, fmt.Errorf("failed to run bootc install mount --help: %w, stderr:\n%s", err, exitErr.Stderr)
}
return false, fmt.Errorf("failed to run bootc install mount --help: %w", err)
}
return true, nil
}

func findImageIdFor(cntId, ref string, extraOpts []string) (string, error) {
args := []string{"inspect"}
args = append(args, extraOpts...)
Expand Down
28 changes: 28 additions & 0 deletions pkg/bootc/resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,34 @@ func TestUnifiedKernelHappy(t *testing.T) {
}
}

func TestHasInstallMount(t *testing.T) {
for _, tc := range []struct {
name string
exitCode int
expected bool
errMsg string
}{
{"supported", 0, true, ""},
{"unknown command", 2, false, ""},
{"error", 1, false, "failed to run bootc install mount --help"},
{"no bootc", 127, false, "failed to run bootc install mount --help"},
} {
t.Run(tc.name, func(t *testing.T) {
makeFakePodman(t, fmt.Sprintf(`#!/bin/sh
exit %d
`, tc.exitCode))
cnt := bootc.Container{}
installMount, err := cnt.HasInstallMount()
if tc.errMsg != "" {
assert.ErrorContains(t, err, tc.errMsg)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tc.expected, installMount)
})
}
}

func TestBootloaderHappy(t *testing.T) {
for _, tc := range []struct {
In string
Expand Down
5 changes: 5 additions & 0 deletions pkg/distro/bootc/bootctest/exe/bootc.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ func fakeBootc() error {
fmt.Println(`{"kernel": {"unified": false}}`)
return nil
}
if len(os.Args) >= 3 && os.Args[1] == "install" && os.Args[2] == "mount" {
// like a bootc without "install mount": clap's usage error
fmt.Fprintln(os.Stderr, "error: unrecognized subcommand 'mount'")
os.Exit(2)
}
return fmt.Errorf("unexpected bootc arguments %v", os.Args)
}

Expand Down
4 changes: 4 additions & 0 deletions pkg/distro/defs/bootc.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type BootcDistro struct {
buildSourceInfo *osinfo.Info
unifiedKernel bool
bootloader *string
// the build container's bootc has "bootc install mount"
installMount bool

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

// load image types from bootc-generic-1
Expand Down Expand Up @@ -299,6 +302,7 @@ func (d *BootcDistro) SetBuildContainer(cinfo *bootc.Info) error {
d.buildImgref = cinfo.Imgref
d.buildImageID = cinfo.ImageID
d.buildSourceInfo = cinfo.OSInfo
d.installMount = cinfo.InstallMount

return nil
}
Expand Down
115 changes: 115 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 Down Expand Up @@ -856,3 +858,116 @@ func TestManifestSubscriptionCustomization(t *testing.T) {
})
}
}

func TestManifestUnifiedKernelCustomizationsWarn(t *testing.T) {
const warnPrefix = `blueprint validation failed for image type "qcow2": the bootc container has a unified kernel (UKI), 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
// what is left when the build container's bootc has "install
// mount", which mounts the deployment for /etc and /var
expectedInstallMount string
}{
"empty": {},
"disk-root-only": {
customizations: &blueprint.Customizations{
Disk: &blueprint.DiskCustomization{Partitions: []blueprint.PartitionCustomization{rootPart}},
},
},
"user-and-kargs": {
customizations: &blueprint.Customizations{
User: []blueprint.UserCustomization{{Name: "alice"}},
Kernel: &blueprint.KernelCustomization{Append: "debug"},
},
expected: "customizations.user, customizations.kernel.append (" + ukiNeedsInstallMount + ")",
expectedInstallMount: "customizations.kernel.append",
},
"group": {
customizations: &blueprint.Customizations{Group: []blueprint.GroupCustomization{{Name: "wheel2"}}},
expected: "customizations.group (" + ukiNeedsInstallMount + ")",
},
"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 (" + ukiNeedsInstallMount + ")",
},
"ignition": {
customizations: &blueprint.Customizations{
Ignition: &blueprint.IgnitionCustomization{FirstBoot: &blueprint.FirstBootIgnitionCustomization{ProvisioningURL: "https://example.com/config.ign"}},
},
expected: "customizations.ignition",
expectedInstallMount: "customizations.ignition",
},
"bootloader": {
customizations: getBootloaderConfig().Customizations,
expected: "customizations.bootloader",
expectedInstallMount: "customizations.bootloader",
},
"subscription": {
options: distro.ImageOptions{
Subscription: &subscription.ImageOptions{Organization: "2040324", ActivationKey: "my-secret-key"},
},
expected: "subscription (" + ukiNeedsInstallMount + ")",
},
"disk-extra-mountpoint": {
customizations: &blueprint.Customizations{
Disk: &blueprint.DiskCustomization{Partitions: []blueprint.PartitionCustomization{rootPart, dataPart}},
},
expected: "customizations.disk mountpoints without an fstab (/var/data)",
expectedInstallMount: "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)",
expectedInstallMount: "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}

// only look at the UKI warning, e.g. customizations.filesystem
// is not a supported option for bootc disks in the first place
ukiWarnings := 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, warnPrefix)
})
}

// without a UKI all of these are applied
assert.Empty(t, ukiWarnings())

bd := imgType.arch.distro.(*BootcDistro)
bd.unifiedKernel = true
for _, installMount := range []bool{false, true} {
bd.installMount = installMount
expected := tc.expected
if installMount {
expected = tc.expectedInstallMount
}
if expected == "" {
assert.Empty(t, ukiWarnings(), "install mount: %v", installMount)
} else {
assert.Equal(t, []string{warnPrefix + expected}, ukiWarnings(), "install mount: %v", installMount)
}
}
})
}
}
Loading