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
47 changes: 44 additions & 3 deletions doc/20-advanced/20-bootc/05-sources-of-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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:

Expand Down
39 changes: 20 additions & 19 deletions pkg/bib/osinfo/osinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

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.

Not at all a blocker, but I wonder if it could throw an error or warning for ignored blueprint entries/customizations here.

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 {
Expand Down Expand Up @@ -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
}
Expand Down
57 changes: 56 additions & 1 deletion pkg/bib/osinfo/osinfo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
Loading