diff --git a/doc/20-advanced/20-bootc/05-sources-of-configuration.md b/doc/20-advanced/20-bootc/05-sources-of-configuration.md index 607c583ea0..21114d7d46 100644 --- a/doc/20-advanced/20-bootc/05-sources-of-configuration.md +++ b/doc/20-advanced/20-bootc/05-sources-of-configuration.md @@ -275,9 +275,48 @@ partition_table: > [!WARNING] > *LUKS configurations currently do not work with bootable containers in `image-builder`. See [here](https://github.com/osbuild/images/issues/2228).* +### `blueprint.json` / `blueprint.toml` + +A JSON or TOML file containing blueprint customizations to apply when building an image from the container. The canonical location is `/usr/lib/image-builder/bootc/blueprint.json` (or `blueprint.toml`). Only the `customizations` section of the blueprint is used; fields like `packages` do not apply to bootc images since the package set comes from the container itself. + +This allows containers to ship default customizations (users, kernel arguments, services, firewall rules, etc.) so that images built from the container are configured correctly without requiring the end-user to supply a separate blueprint. + +Example `blueprint.json`: + +```json +{ + "customizations": { + "user": [ + { + "name": "admin", + "groups": ["wheel"] + } + ], + "kernel": { + "append": "quiet" + } + } +} +``` + +Equivalent `blueprint.toml`: + +```toml +[[customizations.user]] +name = "admin" +groups = ["wheel"] + +[customizations.kernel] +append = "quiet" +``` + +For backward compatibility, `config.json` and `config.toml` are also recognized at the same locations. The `blueprint.*` filenames take precedence when both exist. + +Like `disk.yaml`, these files support [deployment variant](#deployment-variants) overrides. + ### Deployment Variants -Containers can ship multiple `disk.yaml`, `iso.yaml`, and/or `extras.yaml` configurations as *deployment variants*. Variants allow a single container image to produce different disk layouts depending on the target environment, for example a `secure-execution` variant with verity partitions for s390x, or `btrfs` vs `ext4` variants for Fedora images. +Containers can ship multiple `disk.yaml`, `iso.yaml`, `extras.yaml`, and/or `blueprint.json`/`blueprint.toml` configurations as *deployment variants*. Variants allow a single container image to produce different configurations depending on the target environment, for example a `secure-execution` variant with verity partitions for s390x, or `btrfs` vs `ext4` variants for Fedora images. Variants are placed in the `variant.d/` subdirectory, with each variant in its own named directory: @@ -286,15 +325,17 @@ Variants are placed in the `variant.d/` subdirectory, with each variant in its o ├── disk.yaml # default configuration ├── iso.yaml # default ISO configuration ├── extras.yaml # default extras configuration +├── blueprint.json # default blueprint customizations └── variant.d/ ├── btrfs/ │ ├── disk.yaml # btrfs partition layout │ └── extras.yaml # btrfs-specific extras └── secure-execution/ - └── disk.yaml # s390x SE partition layout + ├── disk.yaml # s390x SE partition layout + └── blueprint.json # s390x SE customizations ``` -Each variant directory may contain a `disk.yaml`, `iso.yaml`, and/or `extras.yaml`. When a variant is selected at build time with `--bootc-variant`, `image-builder` uses the variant's configuration files. For any file not provided by the variant, `image-builder` falls back to the default configuration. +Each variant directory may contain a `disk.yaml`, `iso.yaml`, `extras.yaml`, and/or `blueprint.json`/`blueprint.toml`. When a variant is selected at build time with `--bootc-variant`, `image-builder` uses the variant's configuration files. For any file not provided by the variant, `image-builder` falls back to the default configuration. Users can list available variants and select one at build time: diff --git a/pkg/bib/osinfo/osinfo.go b/pkg/bib/osinfo/osinfo.go index 43c156f11b..0caa9a09f2 100644 --- a/pkg/bib/osinfo/osinfo.go +++ b/pkg/bib/osinfo/osinfo.go @@ -192,28 +192,29 @@ func readSelinuxPolicy(fsys fs.FS) (string, error) { return policy, nil } -func readImageCustomization(fsys fs.FS) (*blueprint.Customizations, error) { - // note that we only look at the 'old' search path here, we do want to - // look in the new path as well but i'd like to only support the actual - // blueprint format there instead of buildconfig as well - prefix := searchPaths[1] - - config, err := blueprintload.LoadFS(fsys, path.Join(prefix, "config.json")) - if err != nil && !os.IsNotExist(err) { - return nil, err - } - if config == nil { - config, err = blueprintload.LoadFS(fsys, path.Join(prefix, "config.toml")) - if err != nil && !os.IsNotExist(err) { - return nil, err +func readImageCustomization(fsys fs.FS, prefix, variant string) (*blueprint.Customizations, error) { + searchDirs := []string{prefix} + if variant != "" { + searchDirs = []string{ + path.Join(prefix, "variant.d", variant), + prefix, } } - // no config found in either toml/json - if config == nil { - return nil, nil + + for _, dir := range searchDirs { + for _, filename := range []string{"blueprint.json", "blueprint.toml", "config.json", "config.toml"} { + config, err := blueprintload.LoadFS(fsys, path.Join(dir, filename)) + if err != nil { + if os.IsNotExist(err) { + continue + } + return nil, err + } + return config.Customizations, nil + } } - return config.Customizations, nil + return nil, nil } type diskYAML struct { @@ -438,7 +439,7 @@ func Load(fsys fs.FS, variant string) (*Info, error) { return nil, fmt.Errorf("variant %q not found", variant) } - customization, err := readImageCustomization(fsys) + customization, err := readImageCustomization(fsys, prefix, variant) if err != nil { return nil, err } diff --git a/pkg/bib/osinfo/osinfo_test.go b/pkg/bib/osinfo/osinfo_test.go index 7838801a80..c32fda660a 100644 --- a/pkg/bib/osinfo/osinfo_test.go +++ b/pkg/bib/osinfo/osinfo_test.go @@ -57,10 +57,13 @@ func createBootupdEFI(t *testing.T, root, uefiVendor string) { require.NoError(t, err) } -func createImageCustomization(t *testing.T, root, custType string) { +func createImageCustomization(t *testing.T, root, custType string, variant ...string) { t.Helper() bibDir := path.Join(root, "usr/lib/bootc-image-builder/") + if len(variant) > 0 && variant[0] != "" { + bibDir = path.Join(root, "usr/lib/bootc-image-builder/variant.d", variant[0]) + } err := os.MkdirAll(bibDir, 0755) require.NoError(t, err) @@ -179,6 +182,58 @@ func TestLoadInfo(t *testing.T) { } } +var testCustomizationJSON = `{ + "customizations": { + "disk": { + "partitions": [ + { + "label": "var", + "mountpoint": "/var", + "fs_type": "ext4", + "minsize": "3 GiB" + } + ] + } + } +}` + +func TestLoadInfoVariantCustomization(t *testing.T) { + root := t.TempDir() + writeOSRelease(t, root, "fedora", "40", "Fedora Linux", "platform:f40", "coreos", "") + createBootupdEFI(t, root, "fedora") + + variantDir := path.Join(root, "usr/lib/image-builder/bootc/variant.d/myvariant") + require.NoError(t, os.MkdirAll(variantDir, 0755)) + require.NoError(t, os.WriteFile(path.Join(variantDir, "blueprint.json"), []byte(testCustomizationJSON), 0644)) + + info, err := Load(os.DirFS(root), "myvariant") + require.NoError(t, err) + require.NotNil(t, info.ImageCustomization) + require.NotNil(t, info.ImageCustomization.Disk) + require.NotEmpty(t, info.ImageCustomization.Disk.Partitions) + assert.Equal(t, "var", info.ImageCustomization.Disk.Partitions[0].Label) +} + +func TestLoadInfoVariantFallback(t *testing.T) { + root := t.TempDir() + writeOSRelease(t, root, "fedora", "40", "Fedora Linux", "platform:f40", "coreos", "") + createBootupdEFI(t, root, "fedora") + + baseDir := path.Join(root, "usr/lib/image-builder/bootc") + require.NoError(t, os.MkdirAll(baseDir, 0755)) + require.NoError(t, os.WriteFile(path.Join(baseDir, "blueprint.json"), []byte(testCustomizationJSON), 0644)) + + variantDir := path.Join(baseDir, "variant.d/myvariant") + require.NoError(t, os.MkdirAll(variantDir, 0755)) + + info, err := Load(os.DirFS(root), "myvariant") + require.NoError(t, err) + require.NotNil(t, info.ImageCustomization) + require.NotNil(t, info.ImageCustomization.Disk) + require.NotEmpty(t, info.ImageCustomization.Disk.Partitions) + assert.Equal(t, "var", info.ImageCustomization.Disk.Partitions[0].Label) +} + func TestLoadInfoKernel(t *testing.T) { type testCase struct { desc string