From 4304be684492863ed89af677da3a3a54882c580a Mon Sep 17 00:00:00 2001 From: Simon de Vlieger Date: Mon, 22 Jun 2026 12:11:10 +0200 Subject: [PATCH 1/4] pkg/osbuild: pipeline mounts Add pipeline-level mounts to the Pipeline struct. Mounts set via SetMounts are appended to every stage added through AddStage or AddStages. For stages that intentionally bypass pipeline mounts (e.g. stages that write outside the deployment), AddStageDirect is provided. Signed-off-by: Simon de Vlieger --- pkg/osbuild/osbuild.go | 18 ++++++++ pkg/osbuild/osbuild_test.go | 83 +++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/pkg/osbuild/osbuild.go b/pkg/osbuild/osbuild.go index ac7aa7bb21..2276bbd089 100644 --- a/pkg/osbuild/osbuild.go +++ b/pkg/osbuild/osbuild.go @@ -31,6 +31,8 @@ type Pipeline struct { // Sequence of stages that produce the filesystem tree, which is the // payload of the produced image. Stages []*Stage `json:"stages,omitempty"` + + mounts []Mount } // SetBuild sets the pipeline and runner for generating the build environment @@ -39,10 +41,26 @@ func (p *Pipeline) SetBuild(build string) { p.Build = build } +// SetMounts sets pipeline-level mounts that are appended to every stage +// added via AddStage or AddStages. +func (p *Pipeline) SetMounts(mounts ...Mount) { + p.mounts = append(p.mounts, mounts...) +} + // AddStage appends a stage to the list of stages of a pipeline. The stages // will be executed in the order they are appended. // If the argument is nil, it is not added. func (p *Pipeline) AddStage(stage *Stage) { + if stage != nil { + stage.Mounts = append(stage.Mounts, p.mounts...) + p.Stages = append(p.Stages, stage) + } +} + +// AddStageDirect appends a stage without merging pipeline-level mounts or +// devices. Use this for stages that intentionally bypass the pipeline +// mounts (e.g. stages that write outside the deployment). +func (p *Pipeline) AddStageDirect(stage *Stage) { if stage != nil { p.Stages = append(p.Stages, stage) } diff --git a/pkg/osbuild/osbuild_test.go b/pkg/osbuild/osbuild_test.go index 32a2ee1fb0..8787ff8450 100644 --- a/pkg/osbuild/osbuild_test.go +++ b/pkg/osbuild/osbuild_test.go @@ -27,6 +27,89 @@ func TestPipeline_AddStage(t *testing.T) { assert.Equal(t, 1, len(actualPipeline.Stages)) } +func TestPipeline_SetMounts(t *testing.T) { + pipeline := &Pipeline{} + pipeline.SetMounts( + *NewExt4Mount("root", "root-dev", "/"), + *NewXfsMount("data", "data-dev", "/data"), + ) + + pipeline.AddStage(&Stage{Type: "org.osbuild.rpm"}) + pipeline.AddStage(&Stage{Type: "org.osbuild.locale"}) + + assert.Len(t, pipeline.Stages, 2) + for _, stage := range pipeline.Stages { + assert.Len(t, stage.Mounts, 2) + assert.Equal(t, "root", stage.Mounts[0].Name) + assert.Equal(t, "data", stage.Mounts[1].Name) + } +} + +func TestPipeline_SetMountsAppendsToStageMounts(t *testing.T) { + pipeline := &Pipeline{} + pipeline.SetMounts(*NewExt4Mount("root", "root-dev", "/")) + + stage := &Stage{ + Type: "org.osbuild.rpm", + Mounts: []Mount{*NewXfsMount("existing", "existing-dev", "/existing")}, + } + pipeline.AddStage(stage) + + assert.Len(t, pipeline.Stages[0].Mounts, 2) + assert.Equal(t, "existing", pipeline.Stages[0].Mounts[0].Name) + assert.Equal(t, "root", pipeline.Stages[0].Mounts[1].Name) +} + +func TestPipeline_AddStageNilSkipsMounts(t *testing.T) { + pipeline := &Pipeline{} + pipeline.SetMounts(*NewExt4Mount("root", "root-dev", "/")) + pipeline.AddStage(nil) + + assert.Len(t, pipeline.Stages, 0) +} + +func TestPipeline_AddStagesWithMounts(t *testing.T) { + pipeline := &Pipeline{} + pipeline.SetMounts(*NewFATMount("efi", "efi-dev", "/boot/efi")) + + stages := []*Stage{ + {Type: "org.osbuild.rpm"}, + {Type: "org.osbuild.locale"}, + {Type: "org.osbuild.hostname"}, + } + pipeline.AddStages(stages...) + + assert.Len(t, pipeline.Stages, 3) + for _, stage := range pipeline.Stages { + assert.Len(t, stage.Mounts, 1) + assert.Equal(t, "efi", stage.Mounts[0].Name) + } +} + +func TestPipeline_AddStageDirect(t *testing.T) { + pipeline := &Pipeline{} + pipeline.SetMounts(*NewExt4Mount("root", "root-dev", "/")) + + pipeline.AddStageDirect(&Stage{Type: "org.osbuild.ignition"}) + + assert.Len(t, pipeline.Stages, 1) + assert.Len(t, pipeline.Stages[0].Mounts, 0) +} + +func TestPipeline_AddStageDirectNil(t *testing.T) { + pipeline := &Pipeline{} + pipeline.AddStageDirect(nil) + + assert.Len(t, pipeline.Stages, 0) +} + +func TestPipeline_NoMountsDoesNotAffectStages(t *testing.T) { + pipeline := &Pipeline{} + pipeline.AddStage(&Stage{Type: "org.osbuild.rpm"}) + + assert.Len(t, pipeline.Stages[0].Mounts, 0) +} + var fakeOsbuildManifestWithIdentifiers = []byte(`{ "version": "2", "pipelines": [ From fa7b4ee9043faac96241bda4ad51e1c11fdc0ee0 Mon Sep 17 00:00:00 2001 From: Simon de Vlieger Date: Mon, 22 Jun 2026 12:22:41 +0200 Subject: [PATCH 2/4] pkg/manifest: pipeline mounts for ostree Use the pipeline mount mechanism for the ostree deployment pipeline. Stages that intentionally bypass the pipeline mounts (ignition, selinux) use AddStageDirect. Signed-off-by: Simon de Vlieger --- pkg/manifest/export_test.go | 2 +- pkg/manifest/ostree_deployment.go | 86 +++++++++++-------------------- 2 files changed, 31 insertions(+), 57 deletions(-) diff --git a/pkg/manifest/export_test.go b/pkg/manifest/export_test.go index 40778d88bc..8923d6ab61 100644 --- a/pkg/manifest/export_test.go +++ b/pkg/manifest/export_test.go @@ -50,7 +50,7 @@ func NewTestOSWithPlatform(pf *platform.Data) *OS { } func (p *OSTreeDeployment) AddStagesForAllFilesAndInlineData(pipeline *osbuild.Pipeline, files []*fsnode.File) { - p.addStagesForAllFilesAndInlineData(pipeline, files, "ostree/ref") + p.addStagesForAllFilesAndInlineData(pipeline, files) } func (p *Vagrant) GetMacAddress() string { diff --git a/pkg/manifest/ostree_deployment.go b/pkg/manifest/ostree_deployment.go index 85a4569b9f..2e36353db5 100644 --- a/pkg/manifest/ostree_deployment.go +++ b/pkg/manifest/ostree_deployment.go @@ -337,7 +337,11 @@ func (p *OSTreeDeployment) serialize() (osbuild.Pipeline, error) { return osbuild.Pipeline{}, fmt.Errorf("no content source defined for ostree deployment") } - configStage := osbuild.NewOSTreeConfigStage( + // Set up mounts for the pipeline. These are used by all stages in + // this pipeline except those added via AddStageDirect. + pipeline.SetMounts(*osbuild.NewOSTreeDeploymentMount("ostree-"+ref, p.osName, ref, 0)) + + pipeline.AddStage(osbuild.NewOSTreeConfigStage( &osbuild.OSTreeConfigStageOptions{ Repo: repoPath, Config: &osbuild.OSTreeConfig{ @@ -347,23 +351,16 @@ func (p *OSTreeDeployment) serialize() (osbuild.Pipeline, error) { }, }, }, - ) - configStage.MountOSTree(p.osName, ref, 0) - pipeline.AddStage(configStage) + )) fsCfgStages, err := filesystemConfigStages(p.PartitionTable, p.MountConfiguration) if err != nil { return osbuild.Pipeline{}, err } - for _, stage := range fsCfgStages { - stage.MountOSTree(p.osName, ref, 0) - pipeline.AddStage(stage) - } + pipeline.AddStages(fsCfgStages...) if len(p.Groups) > 0 { - grpStage := osbuild.GenGroupsStage(p.Groups) - grpStage.MountOSTree(p.osName, ref, 0) - pipeline.AddStage(grpStage) + pipeline.AddStage(osbuild.GenGroupsStage(p.Groups)) } if len(p.Users) > 0 { @@ -371,12 +368,13 @@ func (p *OSTreeDeployment) serialize() (osbuild.Pipeline, error) { if err != nil { return osbuild.Pipeline{}, fmt.Errorf("password encryption failed") } - usersStage.MountOSTree(p.osName, ref, 0) pipeline.AddStage(usersStage) } if p.IgnitionPlatform != "" { - pipeline.AddStage(osbuild.NewIgnitionStage(&osbuild.IgnitionStageOptions{ + // The ignition stage writes outside the deployment, so it + // bypasses the pipeline mounts. + pipeline.AddStageDirect(osbuild.NewIgnitionStage(&osbuild.IgnitionStageOptions{ // This is a workaround to make the systemd believe it's firstboot when ignition runs on real firstboot. // Right now, since we ship /etc/machine-id, systemd thinks it's not firstboot and ignition depends on it // to run on the real firstboot to enable services from presets. @@ -399,9 +397,7 @@ func (p *OSTreeDeployment) serialize() (osbuild.Pipeline, error) { if err != nil { return osbuild.Pipeline{}, err } - stageOption := osbuild.NewSystemdUnitCreateStage(mps) - stageOption.MountOSTree(p.osName, ref, 0) - pipeline.AddStage(stageOption) + pipeline.AddStage(osbuild.NewSystemdUnitCreateStage(mps)) p.EnabledServices = append(p.EnabledServices, serviceName) } @@ -412,9 +408,7 @@ func (p *OSTreeDeployment) serialize() (osbuild.Pipeline, error) { // in the code based on distro version, we enable / disable services also by // creating a preset file. if len(p.EnabledServices) != 0 || len(p.DisabledServices) != 0 { - presetsStage := osbuild.GenServicesPresetStage(p.EnabledServices, p.DisabledServices) - presetsStage.MountOSTree(p.osName, ref, 0) - pipeline.AddStage(presetsStage) + pipeline.AddStage(osbuild.GenServicesPresetStage(p.EnabledServices, p.DisabledServices)) } } @@ -428,35 +422,24 @@ func (p *OSTreeDeployment) serialize() (osbuild.Pipeline, error) { } if p.LockRootUser && !hasRoot { - rootLockStage := osbuild.NewUsersStage(osbuild.NewUsersStageOptionsForLockedRoot()) - rootLockStage.MountOSTree(p.osName, ref, 0) - pipeline.AddStage(rootLockStage) + pipeline.AddStage(osbuild.NewUsersStage(osbuild.NewUsersStageOptionsForLockedRoot())) } if p.Keyboard != "" { - options := &osbuild.KeymapStageOptions{ + pipeline.AddStage(osbuild.NewKeymapStage(&osbuild.KeymapStageOptions{ Keymap: p.Keyboard, - } - keymapStage := osbuild.NewKeymapStage(options) - keymapStage.MountOSTree(p.osName, ref, 0) - pipeline.AddStage(keymapStage) + })) } if p.Locale != "" { - options := &osbuild.LocaleStageOptions{ + pipeline.AddStage(osbuild.NewLocaleStage(&osbuild.LocaleStageOptions{ Language: p.Locale, - } - localeStage := osbuild.NewLocaleStage(options) - localeStage.MountOSTree(p.osName, ref, 0) - pipeline.AddStage(localeStage) + })) } if p.FIPS { - p.addStagesForAllFilesAndInlineData(&pipeline, osbuild.GenFIPSFiles(), ref) - for _, stage := range osbuild.GenFIPSStages() { - stage.MountOSTree(p.osName, ref, 0) - pipeline.AddStage(stage) - } + p.addStagesForAllFilesAndInlineData(&pipeline, osbuild.GenFIPSFiles()) + pipeline.AddStages(osbuild.GenFIPSStages()...) } if !p.UseBootupd { @@ -473,34 +456,28 @@ func (p *OSTreeDeployment) serialize() (osbuild.Pipeline, error) { Timeout: 1, TerminalOutput: []string{"console"}, } - bootloader := osbuild.NewGRUB2Stage(grubOptions) - bootloader.MountOSTree(p.osName, ref, 0) - pipeline.AddStage(bootloader) + pipeline.AddStage(osbuild.NewGRUB2Stage(grubOptions)) } // First create custom directories, because some of the files may depend on them if len(p.Directories) > 0 { - dirStages := osbuild.GenDirectoryNodesStages(p.Directories) - for _, stage := range dirStages { - stage.MountOSTree(p.osName, ref, 0) - } - pipeline.AddStages(dirStages...) + pipeline.AddStages(osbuild.GenDirectoryNodesStages(p.Directories)...) } if len(p.Files) > 0 { - p.addStagesForAllFilesAndInlineData(&pipeline, p.Files, ref) + p.addStagesForAllFilesAndInlineData(&pipeline, p.Files) } if len(p.EnabledServices) != 0 || len(p.DisabledServices) != 0 { - systemdStage := osbuild.NewSystemdStage(&osbuild.SystemdStageOptions{ + pipeline.AddStage(osbuild.NewSystemdStage(&osbuild.SystemdStageOptions{ EnabledServices: p.EnabledServices, DisabledServices: p.DisabledServices, - }) - systemdStage.MountOSTree(p.osName, ref, 0) - pipeline.AddStage(systemdStage) + })) } - pipeline.AddStage(osbuild.NewOSTreeSelinuxStage( + // The selinux stage references the deployment through its options, + // not through mounts, so it bypasses the pipeline mounts. + pipeline.AddStageDirect(osbuild.NewOSTreeSelinuxStage( &osbuild.OSTreeSelinuxStageOptions{ Deployment: osbuild.OSTreeDeployment{ OSName: p.osName, @@ -519,11 +496,8 @@ func (p *OSTreeDeployment) getInline() []string { // addStagesForAllFilesAndInlineData generates stages for creating files and adds them to // the pipeline. It also adds their data to the inlineData for the pipeline so // that the appropriate sources are created. -func (p *OSTreeDeployment) addStagesForAllFilesAndInlineData(pipeline *osbuild.Pipeline, files []*fsnode.File, ref string) { - for _, stage := range osbuild.GenFileNodesStages(files) { - stage.MountOSTree(p.osName, ref, 0) - pipeline.AddStage(stage) - } +func (p *OSTreeDeployment) addStagesForAllFilesAndInlineData(pipeline *osbuild.Pipeline, files []*fsnode.File) { + pipeline.AddStages(osbuild.GenFileNodesStages(files)...) for _, file := range files { p.inlineData = append(p.inlineData, string(file.Data())) From 0dca02f86fd49ecc5ad3364664ff705bcbeb1993 Mon Sep 17 00:00:00 2001 From: Simon de Vlieger Date: Mon, 22 Jun 2026 12:26:42 +0200 Subject: [PATCH 3/4] pkg/osbuild: pipeline devices Add pipeline-level devices to the Pipeline struct. Devices set via SetDevices are merged into every stage added through AddStage or AddStages. A stage that defines a device with the same name as a pipeline device causes a panic. Signed-off-by: Simon de Vlieger --- pkg/osbuild/osbuild.go | 20 ++++++++++++- pkg/osbuild/osbuild_test.go | 56 +++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/pkg/osbuild/osbuild.go b/pkg/osbuild/osbuild.go index 2276bbd089..d3b5e66590 100644 --- a/pkg/osbuild/osbuild.go +++ b/pkg/osbuild/osbuild.go @@ -32,7 +32,8 @@ type Pipeline struct { // payload of the produced image. Stages []*Stage `json:"stages,omitempty"` - mounts []Mount + mounts []Mount + devices map[string]Device } // SetBuild sets the pipeline and runner for generating the build environment @@ -47,12 +48,29 @@ func (p *Pipeline) SetMounts(mounts ...Mount) { p.mounts = append(p.mounts, mounts...) } +// SetDevices sets pipeline-level devices that are merged into every stage +// added via AddStage or AddStages. +func (p *Pipeline) SetDevices(devices map[string]Device) { + p.devices = devices +} + // AddStage appends a stage to the list of stages of a pipeline. The stages // will be executed in the order they are appended. // If the argument is nil, it is not added. func (p *Pipeline) AddStage(stage *Stage) { if stage != nil { stage.Mounts = append(stage.Mounts, p.mounts...) + if len(p.devices) > 0 { + if stage.Devices == nil { + stage.Devices = make(map[string]Device, len(p.devices)) + } + for k, v := range p.devices { + if _, exists := stage.Devices[k]; exists { + panic(fmt.Sprintf("stage %q already defines device %q which conflicts with pipeline device", stage.Type, k)) + } + stage.Devices[k] = v + } + } p.Stages = append(p.Stages, stage) } } diff --git a/pkg/osbuild/osbuild_test.go b/pkg/osbuild/osbuild_test.go index 8787ff8450..bdb936d6ba 100644 --- a/pkg/osbuild/osbuild_test.go +++ b/pkg/osbuild/osbuild_test.go @@ -110,6 +110,62 @@ func TestPipeline_NoMountsDoesNotAffectStages(t *testing.T) { assert.Len(t, pipeline.Stages[0].Mounts, 0) } +func TestPipeline_SetDevices(t *testing.T) { + pipeline := &Pipeline{} + pipeline.SetDevices(map[string]Device{ + "root": {Type: "org.osbuild.loopback"}, + "boot": {Type: "org.osbuild.loopback"}, + }) + + pipeline.AddStage(&Stage{Type: "org.osbuild.rpm"}) + pipeline.AddStage(&Stage{Type: "org.osbuild.locale"}) + + assert.Len(t, pipeline.Stages, 2) + for _, stage := range pipeline.Stages { + assert.Len(t, stage.Devices, 2) + assert.Equal(t, "org.osbuild.loopback", stage.Devices["root"].Type) + assert.Equal(t, "org.osbuild.loopback", stage.Devices["boot"].Type) + } +} + +func TestPipeline_SetDevicesPanicsOnConflict(t *testing.T) { + pipeline := &Pipeline{} + pipeline.SetDevices(map[string]Device{ + "root": {Type: "org.osbuild.loopback"}, + }) + + stage := &Stage{ + Type: "org.osbuild.rpm", + Devices: map[string]Device{ + "root": {Type: "org.osbuild.luks2"}, + }, + } + assert.PanicsWithValue(t, + `stage "org.osbuild.rpm" already defines device "root" which conflicts with pipeline device`, + func() { pipeline.AddStage(stage) }, + ) +} + +func TestPipeline_NoDevicesDoesNotAffectStages(t *testing.T) { + pipeline := &Pipeline{} + pipeline.AddStage(&Stage{Type: "org.osbuild.rpm"}) + + assert.Nil(t, pipeline.Stages[0].Devices) +} + +func TestPipeline_MountsAndDevicesTogether(t *testing.T) { + pipeline := &Pipeline{} + pipeline.SetMounts(*NewExt4Mount("root", "root-dev", "/")) + pipeline.SetDevices(map[string]Device{ + "root-dev": {Type: "org.osbuild.loopback"}, + }) + + pipeline.AddStage(&Stage{Type: "org.osbuild.rpm"}) + + assert.Len(t, pipeline.Stages[0].Mounts, 1) + assert.Len(t, pipeline.Stages[0].Devices, 1) +} + var fakeOsbuildManifestWithIdentifiers = []byte(`{ "version": "2", "pipelines": [ From ef8b307b92afeaca00b4e36565f4120b6628963f Mon Sep 17 00:00:00 2001 From: Simon de Vlieger Date: Mon, 22 Jun 2026 12:39:09 +0200 Subject: [PATCH 4/4] pkg/manifest: pipeline mounts for bootc Use the pipeline mount and device mechanism for the bootc pipeline. Stages that intentionally bypass pipeline mounts (ignition, LiveBoot mkdir) use AddStageDirect. Signed-off-by: Simon de Vlieger --- pkg/manifest/raw_bootc.go | 63 +++++++++++++-------------------------- 1 file changed, 20 insertions(+), 43 deletions(-) diff --git a/pkg/manifest/raw_bootc.go b/pkg/manifest/raw_bootc.go index 91c896bb46..ff4a722abb 100644 --- a/pkg/manifest/raw_bootc.go +++ b/pkg/manifest/raw_bootc.go @@ -189,46 +189,39 @@ func (p *RawBootcImage) serialize() (osbuild.Pipeline, error) { mounts = append(mounts, *osbuild.NewOSTreeDeploymentMountDefault("ostree.deployment", osbuild.OSTreeMountSourceMount)) mounts = append(mounts, *osbuild.NewBindMount("bind-ostree-deployment-to-tree", "mount://", "tree://")) + pipeline.SetMounts(mounts...) + pipeline.SetDevices(devices) + postStages := []*osbuild.Stage{} fsCfgStages, err := filesystemConfigStages(pt, p.DiskCustomizations.MountConfiguration) if err != nil { return osbuild.Pipeline{}, err } - for _, stage := range fsCfgStages { - stage.Mounts = mounts - stage.Devices = devices - postStages = append(postStages, stage) - } + postStages = append(postStages, fsCfgStages...) // customize the image if len(p.OSCustomizations.Groups) > 0 { - groupsStage := osbuild.GenGroupsStage(p.OSCustomizations.Groups) - groupsStage.Mounts = mounts - groupsStage.Devices = devices - postStages = append(postStages, groupsStage) + postStages = append(postStages, osbuild.GenGroupsStage(p.OSCustomizations.Groups)) } if len(p.OSCustomizations.Users) > 0 { // ensure home root dir (currently /var/home, /var/roothome) is // available - mkdirStage := osbuild.NewMkdirStage(&osbuild.MkdirStageOptions{ + postStages = append(postStages, osbuild.NewMkdirStage(&osbuild.MkdirStageOptions{ Paths: buildHomedirPaths(p.OSCustomizations.Users), - }) - mkdirStage.Mounts = mounts - mkdirStage.Devices = devices - postStages = append(postStages, mkdirStage) + })) // add the users usersStage, err := osbuild.GenUsersStage(p.OSCustomizations.Users, false) if err != nil { return osbuild.Pipeline{}, fmt.Errorf("user stage failed %w", err) } - usersStage.Mounts = mounts - usersStage.Devices = devices postStages = append(postStages, usersStage) } + pipeline.AddStages(postStages...) + if p.LiveBoot { // The dracut dmsquash-live module has a check for the root filesystem // This is a kludge to work around this: On Fedora and RHEL10 it expects /usr in the root @@ -254,31 +247,22 @@ func (p *RawBootcImage) serialize() (osbuild.Pipeline, error) { } noDeploymentMounts = append(noDeploymentMounts, m) } + // These stages use custom mounts (without the deployment + // mount) so they bypass the pipeline mounts. for _, stage := range stages { stage.Mounts = noDeploymentMounts stage.Devices = devices + pipeline.AddStageDirect(stage) } - postStages = append(postStages, stages...) } // First create custom directories, because some of the custom files may depend on them if len(p.OSCustomizations.Directories) > 0 { - - stages := osbuild.GenDirectoryNodesStages(p.OSCustomizations.Directories) - for _, stage := range stages { - stage.Mounts = mounts - stage.Devices = devices - } - postStages = append(postStages, stages...) + pipeline.AddStages(osbuild.GenDirectoryNodesStages(p.OSCustomizations.Directories)...) } if len(p.OSCustomizations.Files) > 0 { - stages := osbuild.GenFileNodesStages(p.OSCustomizations.Files) - for _, stage := range stages { - stage.Mounts = mounts - stage.Devices = devices - } - postStages = append(postStages, stages...) + pipeline.AddStages(osbuild.GenFileNodesStages(p.OSCustomizations.Files)...) } // The ignition stamp must be created after bootc install, otherwise bootc will error out @@ -299,32 +283,25 @@ func (p *RawBootcImage) serialize() (osbuild.Pipeline, error) { } ignitionStage = osbuild.NewIgnitionStage(&opts) } - var err error - // We cannot reuse the existing mounts because the generated ostree mounts are shadowing /boot and the file ends up - // in the wrong place. We reuse the bootupd mount generator as it's enough for this. We just need /boot. + // The ignition stage uses its own mounts and devices + // because the pipeline ostree mounts shadow /boot. ignitionStage.Devices, ignitionStage.Mounts, err = osbuild.GenBootupdDevicesMounts(p.filename, p.PartitionTable, p.platform) if err != nil { return osbuild.Pipeline{}, fmt.Errorf("gen devices stage failed %w", err) } - postStages = append(postStages, ignitionStage) + pipeline.AddStageDirect(ignitionStage) } - pipeline.AddStages(postStages...) - // In case we created any files in the deploy directory we need to relabel // then per the selinux policy if p.OSCustomizations.SELinux != "" { - if len(postStages) > 0 { + if len(postStages) > 0 || len(p.OSCustomizations.Directories) > 0 || len(p.OSCustomizations.Files) > 0 { for _, changedFile := range []string{"/etc", "/var"} { - opts := &osbuild.SELinuxStageOptions{ + pipeline.AddStage(osbuild.NewSELinuxStage(&osbuild.SELinuxStageOptions{ Target: "tree://" + changedFile, FileContexts: fmt.Sprintf("etc/selinux/%s/contexts/files/file_contexts", p.OSCustomizations.SELinux), ExcludePaths: []string{"/sysroot"}, - } - selinuxStage := osbuild.NewSELinuxStage(opts) - selinuxStage.Mounts = mounts - selinuxStage.Devices = devices - pipeline.AddStage(selinuxStage) + })) } } }