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
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,12 @@ sudo podman run \
The configuration can also be passed in via stdin when `--config -`
is used. Only JSON configuration is supported in this mode.

Additionally, images can embed a build config file, either as
`config.json` or `config.toml` in the `/usr/lib/bootc-image-builder`
directory. If this exist, and contains filesystem or disk
customizations, then these are used by default if no such
customization are specified in the regular build config.

### Users (`user`, array)

Possible fields:
Expand Down Expand Up @@ -534,7 +540,6 @@ By default, the following modules are enabled for all Anaconda ISOs:
The `disable` list is processed after the `enable` list and therefore takes priority. In other words, adding the same module in both `enable` and `disable` will result in the module being **disabled**.
Furthermore, adding a module that is enabled by default to `disable` will result in the module being **disabled**.


## Building

To build the container locally you can run
Expand Down
12 changes: 12 additions & 0 deletions bib/cmd/bootc-image-builder/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,18 @@ func genPartitionTable(c *ManifestConfig, customizations *blueprint.Customizatio
if err != nil {
return nil, fmt.Errorf("error reading disk customizations: %w", err)
}

// Embedded disk customization applies if there was no local customization
if fsCust == nil && diskCust == nil && c.SourceInfo != nil && c.SourceInfo.ImageCustomization != nil {
imageCustomizations := c.SourceInfo.ImageCustomization

fsCust = imageCustomizations.GetFilesystems()
diskCust, err = imageCustomizations.GetPartitioning()
if err != nil {
return nil, fmt.Errorf("error reading disk customizations: %w", err)
}
}

switch {
// XXX: move into images library
case fsCust != nil && diskCust != nil:
Expand Down
10 changes: 10 additions & 0 deletions bib/internal/buildconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,16 @@ func loadConfig(path string) (*externalBlueprint.Blueprint, error) {
}
}

func LoadConfig(path string) (*imagesBlueprint.Blueprint, error) {
externalBp, err := loadConfig(path)
if err != nil {
return nil, err
}

bp := externalBlueprint.Convert(*externalBp)
return &bp, nil
}

func readWithFallback(userConfig string) (*externalBlueprint.Blueprint, error) {
// user asked for an explicit config
if userConfig != "" {
Expand Down
38 changes: 35 additions & 3 deletions bib/internal/source/source.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ import (

"github.com/sirupsen/logrus"

"github.com/osbuild/bootc-image-builder/bib/internal/buildconfig"
"github.com/osbuild/images/pkg/blueprint"
"github.com/osbuild/images/pkg/distro"
)

const bibPathPrefix = "usr/lib/bootc-image-builder"

type OSRelease struct {
PlatformID string
ID string
Expand All @@ -21,8 +25,9 @@ type OSRelease struct {
}

type Info struct {
OSRelease OSRelease
UEFIVendor string
OSRelease OSRelease
UEFIVendor string
ImageCustomization *blueprint.Customizations
}

func validateOSRelease(osrelease map[string]string) error {
Expand Down Expand Up @@ -58,6 +63,26 @@ func uefiVendor(root string) (string, error) {
return "", fmt.Errorf("cannot find UEFI vendor in %s", bootupdEfiDir)
}

func readImageCustomization(root string) (*blueprint.Customizations, error) {
prefix := path.Join(root, bibPathPrefix)
config, err := buildconfig.LoadConfig(path.Join(prefix, "config.json"))
if err != nil && !os.IsNotExist(err) {
return nil, err
}
if config == nil {
config, err = buildconfig.LoadConfig(path.Join(prefix, "config.toml"))
if err != nil && !os.IsNotExist(err) {
return nil, err
}
}
// no config found in either toml/json
if config == nil {
return nil, nil
}

return config.Customizations, nil
}

func LoadInfo(root string) (*Info, error) {
osrelease, err := distro.ReadOSReleaseFromTree(root)
if err != nil {
Expand All @@ -71,6 +96,12 @@ func LoadInfo(root string) (*Info, error) {
if err != nil {
logrus.Debugf("cannot read UEFI vendor: %v, setting it to none", err)
}

customization, err := readImageCustomization(root)
if err != nil {
return nil, err
}

var idLike []string
if osrelease["ID_LIKE"] != "" {
idLike = strings.Split(osrelease["ID_LIKE"], " ")
Expand All @@ -86,6 +117,7 @@ func LoadInfo(root string) (*Info, error) {
IDLike: idLike,
},

UEFIVendor: vendor,
UEFIVendor: vendor,
ImageCustomization: customization,
}, nil
}
86 changes: 77 additions & 9 deletions bib/internal/source/source_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package source

import (
"fmt"
"os"
"path"
"strings"
Expand Down Expand Up @@ -47,6 +48,52 @@ func createBootupdEFI(root, uefiVendor string) error {
return os.Mkdir(path.Join(root, "usr/lib/bootupd/updates/EFI", uefiVendor), 0755)
}

func createImageCustomization(root, custType string) error {
bibDir := path.Join(root, "usr/lib/bootc-image-builder/")
err := os.MkdirAll(bibDir, 0755)
if err != nil {
return err
}

var buf string
var filename string
switch custType {
case "json":
buf = `{
"customizations": {
"disk": {
"partitions": [
{
"label": "var",
"mountpoint": "/var",
"fs_type": "ext4",
"minsize": "3 GiB",
"part_type": "01234567-89ab-cdef-0123-456789abcdef"
}
]
}
}
}`
filename = "config.json"
case "toml":
buf = `[[customizations.disk.partitions]]
label = "var"
mountpoint = "/var"
fs_type = "ext4"
minsize = "3 GiB"
part_type = "01234567-89ab-cdef-0123-456789abcdef"
`
filename = "config.toml"
case "broken":
buf = "{"
filename = "config.json"
default:
return fmt.Errorf("unsupported customization type %s", custType)
}

return os.WriteFile(path.Join(bibDir, filename), []byte(buf), 0644)
}

func TestLoadInfo(t *testing.T) {
cases := []struct {
desc string
Expand All @@ -57,16 +104,20 @@ func TestLoadInfo(t *testing.T) {
platformID string
variantID string
idLike string
custType string
errorStr string
}{
{"happy", "fedora", "40", "Fedora Linux", "fedora", "platform:f40", "coreos", "", ""},
{"happy-no-uefi", "fedora", "40", "Fedora Linux", "", "platform:f40", "coreos", "", ""},
{"happy-no-variant_id", "fedora", "40", "Fedora Linux", "", "platform:f40", "", "", ""},
{"happy-no-id", "fedora", "43", "Fedora Linux", "fedora", "", "", "", ""},
{"happy-with-id-like", "centos", "9", "CentOS Stream", "", "platform:el9", "", "rhel fedora", ""},
{"sad-no-id", "", "40", "Fedora Linux", "fedora", "platform:f40", "", "", "missing ID in os-release"},
{"sad-no-id", "fedora", "", "Fedora Linux", "fedora", "platform:f40", "", "", "missing VERSION_ID in os-release"},
{"sad-no-id", "fedora", "40", "", "fedora", "platform:f40", "", "", "missing NAME in os-release"},
{"happy", "fedora", "40", "Fedora Linux", "fedora", "platform:f40", "coreos", "", "json", ""},
{"happy-no-uefi", "fedora", "40", "Fedora Linux", "", "platform:f40", "coreos", "", "json", ""},
{"happy-no-variant_id", "fedora", "40", "Fedora Linux", "", "platform:f40", "", "", "json", ""},
{"happy-no-id", "fedora", "43", "Fedora Linux", "fedora", "", "", "", "json", ""},
{"happy-with-id-like", "centos", "9", "CentOS Stream", "", "platform:el9", "", "rhel fedora", "json", ""},
{"happy-no-cust", "fedora", "40", "Fedora Linux", "fedora", "platform:f40", "coreos", "", "", ""},
{"happy-toml", "fedora", "40", "Fedora Linux", "fedora", "platform:f40", "coreos", "", "toml", ""},
{"sad-no-id", "", "40", "Fedora Linux", "fedora", "platform:f40", "", "", "json", "missing ID in os-release"},
{"sad-no-id", "fedora", "", "Fedora Linux", "fedora", "platform:f40", "", "", "json", "missing VERSION_ID in os-release"},
{"sad-no-id", "fedora", "40", "", "fedora", "platform:f40", "", "", "json", "missing NAME in os-release"},
{"sad-broken-json", "fedora", "40", "Fedora Linux", "fedora", "platform:f40", "coreos", "", "broken", "cannot decode \"$ROOT/usr/lib/bootc-image-builder/config.json\": unexpected EOF"},
}

for _, c := range cases {
Expand All @@ -76,12 +127,16 @@ func TestLoadInfo(t *testing.T) {
if c.uefiVendor != "" {
require.NoError(t, createBootupdEFI(root, c.uefiVendor))

}
if c.custType != "" {
require.NoError(t, createImageCustomization(root, c.custType))

}

info, err := LoadInfo(root)

if c.errorStr != "" {
require.EqualError(t, err, c.errorStr)
require.EqualError(t, err, strings.ReplaceAll(c.errorStr, "$ROOT", root))
return
}
require.NoError(t, err)
Expand All @@ -91,6 +146,19 @@ func TestLoadInfo(t *testing.T) {
assert.Equal(t, c.uefiVendor, info.UEFIVendor)
assert.Equal(t, c.platformID, info.OSRelease.PlatformID)
assert.Equal(t, c.variantID, info.OSRelease.VariantID)
if c.custType != "" {
assert.NotNil(t, info.ImageCustomization)
assert.NotNil(t, info.ImageCustomization.Disk)
assert.NotEmpty(t, info.ImageCustomization.Disk.Partitions)
part := info.ImageCustomization.Disk.Partitions[0]
assert.Equal(t, part.Label, "var")
assert.Equal(t, part.MinSize, uint64(3*1024*1024*1024))
assert.Equal(t, part.FSType, "ext4")
assert.Equal(t, part.Mountpoint, "/var")
// TODO: Validate part.PartType when it is fixed
} else {
assert.Nil(t, info.ImageCustomization)
}
if c.idLike == "" {
assert.Equal(t, len(info.OSRelease.IDLike), 0)
} else {
Expand Down
3 changes: 2 additions & 1 deletion test/test_build_disk.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ def registry_conf_fixture(shared_tmpdir, request):
"-p", f"{registry_port}:5000",
"--restart", "always",
"--name", registry_container_name,
"registry:2"
# We use a copy of docker.io registry to avoid running into docker.io pull rate limits
"ghcr.io/osbuild/bootc-image-builder/registry:2"
], check=True)

registry_container_state = subprocess.run([
Expand Down
98 changes: 98 additions & 0 deletions test/test_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -826,3 +826,101 @@ def test_manifest_customization_custom_file_smoke(tmp_path, build_container):
'[{"path":"/etc/custom_dir","exist_ok":true}]},'
'"devices":{"disk":{"type":"org.osbuild.loopback"'
',"options":{"filename":"disk.raw"') in output


def find_sfdisk_stage_from(manifest_str):
manifest = json.loads(manifest_str)
for pipl in manifest["pipelines"]:
if pipl["name"] == "image":
for st in pipl["stages"]:
if st["type"] == "org.osbuild.sfdisk":
return st["options"]
raise ValueError(f"cannot find sfdisk stage manifest:\n{manifest_str}")


def test_manifest_image_customize_filesystem(tmp_path, build_container):
# no need to parameterize this test, overrides behaves same for all containers
container_ref = "quay.io/centos-bootc/centos-bootc:stream9"
testutil.pull_container(container_ref)

cfg = {
"blueprint": {
"customizations": {
"filesystem": [
{
"mountpoint": "/boot",
"minsize": "3GiB"
}
]
},
},
}

config_json_path = tmp_path / "config.json"
config_json_path.write_text(json.dumps(cfg), encoding="utf-8")

# create derrived container with filesystem customization
cntf_path = tmp_path / "Containerfile"
cntf_path.write_text(textwrap.dedent(f"""\n
FROM {container_ref}
RUN mkdir -p -m 0755 /usr/lib/bootc-image-builder
COPY config.json /usr/lib/bootc-image-builder/
"""), encoding="utf8")

print(f"building filesystem customize container from {container_ref}")
with make_container(tmp_path) as container_tag:
print(f"using {container_tag}")
manifest_str = subprocess.check_output([
*testutil.podman_run_common,
build_container,
"manifest",
f"localhost/{container_tag}",
], encoding="utf8")
sfdisk_options = find_sfdisk_stage_from(manifest_str)
assert sfdisk_options["partitions"][2]["size"] == 3 * 1024 * 1024 * 1024 / 512


def test_manifest_image_customize_disk(tmp_path, build_container):
# no need to parameterize this test, overrides behaves same for all containers
container_ref = "quay.io/centos-bootc/centos-bootc:stream9"
testutil.pull_container(container_ref)

cfg = {
"blueprint": {
"customizations": {
"disk": {
"partitions": [
{
"label": "var",
"mountpoint": "/var",
"fs_type": "ext4",
"minsize": "3 GiB",
},
],
},
},
},
}

config_json_path = tmp_path / "config.json"
config_json_path.write_text(json.dumps(cfg), encoding="utf-8")

# create derrived container with disk customization
cntf_path = tmp_path / "Containerfile"
cntf_path.write_text(textwrap.dedent(f"""\n
FROM {container_ref}
RUN mkdir -p -m 0755 /usr/lib/bootc-image-builder
COPY config.json /usr/lib/bootc-image-builder/
"""), encoding="utf8")

print(f"building filesystem customize container from {container_ref}")
with make_container(tmp_path) as container_tag:
print(f"using {container_tag}")
manifest_str = subprocess.check_output([
*testutil.podman_run_common,
build_container,
"manifest",
f"localhost/{container_tag}",
], encoding="utf8")
sfdisk_options = find_sfdisk_stage_from(manifest_str)
assert sfdisk_options["partitions"][2]["size"] == 3 * 1024 * 1024 * 1024 / 512
Loading