@@ -186,14 +186,17 @@ func TestRawBootcImageSerializeMkdirOptions(t *testing.T) {
186186 pipeline , err := rawBootcPipeline .Serialize ()
187187 assert .NoError (t , err )
188188
189- mkdirStage := findStage ("org.osbuild.mkdir" , pipeline .Stages )
189+ // Use findPostInstallStages to avoid matching pre-install mkdir
190+ // stages (e.g. mountpoint SELinux labeling) if SELinux is ever
191+ // set in this test.
192+ postInstallMkdirStages := findPostInstallStages ("org.osbuild.mkdir" , pipeline .Stages )
190193 if len (tc .expectedMkdirPaths ) > 0 {
191194 // ensure options got passed
192- require .NotNil (t , mkdirStage )
193- mkdirOptions := mkdirStage .Options .(* osbuild.MkdirStageOptions )
195+ require .Greater (t , len ( postInstallMkdirStages ), 0 )
196+ mkdirOptions := postInstallMkdirStages [ 0 ] .Options .(* osbuild.MkdirStageOptions )
194197 assert .Equal (t , tc .expectedMkdirPaths , mkdirOptions .Paths )
195198 } else {
196- require . Nil (t , mkdirStage )
199+ assert . Equal (t , 0 , len ( postInstallMkdirStages ) )
197200 }
198201 }
199202}
@@ -240,6 +243,29 @@ func assertBootcDeploymentAndBindMount(t *testing.T, stage *osbuild.Stage) {
240243 assert .True (t , bindMntIdx > deploymentMntIdx )
241244}
242245
246+ // findPostInstallStages returns stages that come after the bootc install stage
247+ // and have the ostree deployment + bind mount (i.e. post-install customization stages).
248+ func findPostInstallStages (stageType string , stages []* osbuild.Stage ) []* osbuild.Stage {
249+ // Find the bootc install stage index
250+ bootcIdx := - 1
251+ for i , s := range stages {
252+ if s .Type == "org.osbuild.bootc.install-to-filesystem" {
253+ bootcIdx = i
254+ break
255+ }
256+ }
257+ if bootcIdx < 0 {
258+ return nil
259+ }
260+ var found []* osbuild.Stage
261+ for _ , s := range stages [bootcIdx + 1 :] {
262+ if s .Type == stageType {
263+ found = append (found , s )
264+ }
265+ }
266+ return found
267+ }
268+
243269func TestRawBootcImageSerializeCustomizationGenCorrectStages (t * testing.T ) {
244270 rawBootcPipeline := makeFakeRawBootcPipeline ()
245271
@@ -253,18 +279,21 @@ func TestRawBootcImageSerializeCustomizationGenCorrectStages(t *testing.T) {
253279 {nil , nil , "" , nil },
254280 {[]users.User {{Name : "foo" }}, nil , "" , []string {"org.osbuild.mkdir" , "org.osbuild.users" }},
255281 {[]users.User {{Name : "foo" }}, nil , "targeted" , []string {"org.osbuild.mkdir" , "org.osbuild.users" , "org.osbuild.selinux" }},
256- {[]users.User {{Name : "foo" }}, []users.Group {{Name : "bar" }}, "targeted" , []string {"org.osbuild.mkdir " , "org.osbuild.users " , "org.osbuild.users" , "org.osbuild.selinux" }},
282+ {[]users.User {{Name : "foo" }}, []users.Group {{Name : "bar" }}, "targeted" , []string {"org.osbuild.groups " , "org.osbuild.mkdir " , "org.osbuild.users" , "org.osbuild.selinux" }},
257283 } {
258284 rawBootcPipeline .OSCustomizations .Users = tc .users
285+ rawBootcPipeline .OSCustomizations .Groups = tc .groups
259286 rawBootcPipeline .OSCustomizations .SELinux = tc .SELinux
260287
261288 pipeline , err := rawBootcPipeline .Serialize ()
262289 assert .NoError (t , err )
263290
264291 for _ , expectedStage := range tc .expectedStages {
265- stage := findStage (expectedStage , pipeline .Stages )
266- assert .NotNil (t , stage )
267- assertBootcDeploymentAndBindMount (t , stage )
292+ stages := findPostInstallStages (expectedStage , pipeline .Stages )
293+ assert .Greater (t , len (stages ), 0 , "expected post-install stage %q not found" , expectedStage )
294+ for _ , stage := range stages {
295+ assertBootcDeploymentAndBindMount (t , stage )
296+ }
268297 }
269298 }
270299}
@@ -325,16 +354,15 @@ func TestRawBootcImageSerializeCreateFilesDirs(t *testing.T) {
325354 pipeline , err := rawBootcPipeline .Serialize ()
326355 assert .NoError (t , err )
327356
328- // check dirs
329- mkdirStage := findStage ("org.osbuild.mkdir" , pipeline .Stages )
357+ // check dirs - look for post-install mkdir stages (with deployment mounts)
358+ postInstallMkdirStages := findPostInstallStages ("org.osbuild.mkdir" , pipeline .Stages )
330359 if len (tc .dirs ) > 0 {
331- // ensure options got passed
332- require .NotNil (t , mkdirStage )
333- mkdirOptions := mkdirStage .Options .(* osbuild.MkdirStageOptions )
360+ require .Greater (t , len (postInstallMkdirStages ), 0 )
361+ mkdirOptions := postInstallMkdirStages [0 ].Options .(* osbuild.MkdirStageOptions )
334362 assert .Equal (t , "/path/to/dir" , mkdirOptions .Paths [0 ].Path )
335- assertBootcDeploymentAndBindMount (t , mkdirStage )
363+ assertBootcDeploymentAndBindMount (t , postInstallMkdirStages [ 0 ] )
336364 } else {
337- assert .Nil (t , mkdirStage )
365+ assert .Equal (t , 0 , len ( postInstallMkdirStages ) )
338366 }
339367
340368 // check files
@@ -349,9 +377,15 @@ func TestRawBootcImageSerializeCreateFilesDirs(t *testing.T) {
349377 assert .Nil (t , copyStage )
350378 }
351379
352- selinuxStage := findStage ("org.osbuild.selinux" , pipeline .Stages )
380+ // Pre-install SELinux stages for mountpoint labeling
381+ preInstallSELinuxStages := findPreInstallStages ("org.osbuild.selinux" , pipeline .Stages )
382+ assert .Equal (t , 2 , len (preInstallSELinuxStages ), "expected 2 pre-install selinux stages (root + boot)" )
353383
354- assert .NotNil (t , selinuxStage )
384+ // Post-install SELinux stages for relabeling customizations
385+ if len (tc .dirs ) > 0 || len (tc .files ) > 0 {
386+ postInstallSELinuxStages := findPostInstallStages ("org.osbuild.selinux" , pipeline .Stages )
387+ assert .Greater (t , len (postInstallSELinuxStages ), 0 , "expected post-install selinux relabeling stages" )
388+ }
355389
356390 // XXX: we should really check that the inline
357391 // source for files got generated but that is
@@ -388,6 +422,143 @@ func TestRawBootcPipelineNoMountsStages(t *testing.T) {
388422 checkStagesForNoMounts (t , common .Must (pipeline .Serialize ()).Stages )
389423}
390424
425+ // findPreInstallStages returns stages of the given type that appear before
426+ // the bootc install stage.
427+ func findPreInstallStages (stageType string , stages []* osbuild.Stage ) []* osbuild.Stage {
428+ var found []* osbuild.Stage
429+ for _ , s := range stages {
430+ if s .Type == "org.osbuild.bootc.install-to-filesystem" {
431+ break
432+ }
433+ if s .Type == stageType {
434+ found = append (found , s )
435+ }
436+ }
437+ return found
438+ }
439+
440+ func TestRawBootcImageSerializeMountpointSELinuxLabeling (t * testing.T ) {
441+ rawBootcPipeline := makeFakeRawBootcPipeline ()
442+ rawBootcPipeline .OSCustomizations .SELinux = "targeted"
443+
444+ pipeline , err := rawBootcPipeline .Serialize ()
445+ require .NoError (t , err )
446+
447+ // Pre-install mkdir stages: one for /boot (root-only), one for /boot/efi (root+boot)
448+ preInstallMkdirStages := findPreInstallStages ("org.osbuild.mkdir" , pipeline .Stages )
449+ require .Equal (t , 2 , len (preInstallMkdirStages ))
450+
451+ // First mkdir stage creates /boot with only root mounted
452+ bootMkdirOpts := preInstallMkdirStages [0 ].Options .(* osbuild.MkdirStageOptions )
453+ require .Equal (t , 1 , len (bootMkdirOpts .Paths ))
454+ assert .Equal (t , "mount://-/boot" , bootMkdirOpts .Paths [0 ].Path )
455+ assert .NotEmpty (t , preInstallMkdirStages [0 ].Devices )
456+ bootMkdirMountTargets := make ([]string , 0 )
457+ for _ , mnt := range preInstallMkdirStages [0 ].Mounts {
458+ bootMkdirMountTargets = append (bootMkdirMountTargets , mnt .Target )
459+ }
460+ assert .Contains (t , bootMkdirMountTargets , "/" )
461+ assert .NotContains (t , bootMkdirMountTargets , "/boot" )
462+
463+ // Second mkdir stage creates /boot/efi with root+boot mounted
464+ efiMkdirOpts := preInstallMkdirStages [1 ].Options .(* osbuild.MkdirStageOptions )
465+ require .Equal (t , 1 , len (efiMkdirOpts .Paths ))
466+ assert .Equal (t , "mount://boot/efi" , efiMkdirOpts .Paths [0 ].Path )
467+ assert .NotEmpty (t , preInstallMkdirStages [1 ].Devices )
468+ efiMkdirMountTargets := make ([]string , 0 )
469+ for _ , mnt := range preInstallMkdirStages [1 ].Mounts {
470+ efiMkdirMountTargets = append (efiMkdirMountTargets , mnt .Target )
471+ }
472+ assert .Contains (t , efiMkdirMountTargets , "/" )
473+ assert .Contains (t , efiMkdirMountTargets , "/boot" )
474+
475+ // Pre-install selinux stages: one for root, one for /boot
476+ preInstallSELinuxStages := findPreInstallStages ("org.osbuild.selinux" , pipeline .Stages )
477+ require .Equal (t , 2 , len (preInstallSELinuxStages ))
478+
479+ // First SELinux stage: labels root mount (without boot mounted)
480+ rootSELinuxOpts := preInstallSELinuxStages [0 ].Options .(* osbuild.SELinuxStageOptions )
481+ assert .Equal (t , "mount://-/" , rootSELinuxOpts .Target )
482+ assert .Contains (t , rootSELinuxOpts .FileContexts , "input://tree/etc/selinux/targeted/contexts/files/file_contexts" )
483+ // Should have inputs (tree input from source pipeline)
484+ assert .NotNil (t , preInstallSELinuxStages [0 ].Inputs )
485+ // Should only mount root
486+ rootMountTargets := make ([]string , 0 )
487+ for _ , mnt := range preInstallSELinuxStages [0 ].Mounts {
488+ rootMountTargets = append (rootMountTargets , mnt .Target )
489+ }
490+ assert .Contains (t , rootMountTargets , "/" )
491+ assert .NotContains (t , rootMountTargets , "/boot" )
492+
493+ // Second SELinux stage: labels /boot (with boot mounted)
494+ bootSELinuxOpts := preInstallSELinuxStages [1 ].Options .(* osbuild.SELinuxStageOptions )
495+ assert .Equal (t , "mount://-/boot/" , bootSELinuxOpts .Target )
496+ assert .Contains (t , bootSELinuxOpts .FileContexts , "input://tree/etc/selinux/targeted/contexts/files/file_contexts" )
497+ assert .NotNil (t , preInstallSELinuxStages [1 ].Inputs )
498+ // Should mount both root and boot
499+ bootMountTargets := make ([]string , 0 )
500+ for _ , mnt := range preInstallSELinuxStages [1 ].Mounts {
501+ bootMountTargets = append (bootMountTargets , mnt .Target )
502+ }
503+ assert .Contains (t , bootMountTargets , "/" )
504+ assert .Contains (t , bootMountTargets , "/boot" )
505+ }
506+
507+ func TestRawBootcImageSerializeMountpointSELinuxLabelingNoSELinux (t * testing.T ) {
508+ rawBootcPipeline := makeFakeRawBootcPipeline ()
509+ // No SELinux set - no pre-install labeling stages should be generated
510+ rawBootcPipeline .OSCustomizations .SELinux = ""
511+
512+ pipeline , err := rawBootcPipeline .Serialize ()
513+ require .NoError (t , err )
514+
515+ preInstallSELinuxStages := findPreInstallStages ("org.osbuild.selinux" , pipeline .Stages )
516+ assert .Equal (t , 0 , len (preInstallSELinuxStages ))
517+ preInstallMkdirStages := findPreInstallStages ("org.osbuild.mkdir" , pipeline .Stages )
518+ assert .Equal (t , 0 , len (preInstallMkdirStages ))
519+ }
520+
521+ func TestRawBootcImageSerializeMountpointSELinuxLabelingNoBoot (t * testing.T ) {
522+ // Test the edge case where there is no separate /boot partition
523+ // (only root + EFI). Only the root SELinux stage should be generated,
524+ // no /boot labeling stage.
525+ mani := manifest .New ()
526+ r := & runner.Linux {}
527+ pf := & platform.Data {
528+ Arch : arch .ARCH_X86_64 ,
529+ UEFIVendor : "test" ,
530+ }
531+ build := manifest .NewBuildFromContainer (& mani , r , nil , nil )
532+ rawBootcPipeline := manifest .NewRawBootcImage (build , nil , pf )
533+ rawBootcPipeline .PartitionTable = testdisk .MakeFakePartitionTable ("/" , "/boot/efi" )
534+ err := rawBootcPipeline .SerializeStart (manifest.Inputs {Containers : []container.Spec {{Source : "foo" }}})
535+ require .NoError (t , err )
536+
537+ rawBootcPipeline .OSCustomizations .SELinux = "targeted"
538+
539+ pipeline , err := rawBootcPipeline .Serialize ()
540+ require .NoError (t , err )
541+
542+ // Pre-install mkdir stage should only create /boot (no /boot/efi since
543+ // there's no separate boot partition to mount it on)
544+ preInstallMkdirStages := findPreInstallStages ("org.osbuild.mkdir" , pipeline .Stages )
545+ require .Equal (t , 1 , len (preInstallMkdirStages ))
546+ mkdirOpts := preInstallMkdirStages [0 ].Options .(* osbuild.MkdirStageOptions )
547+ var mkdirPaths []string
548+ for _ , p := range mkdirOpts .Paths {
549+ mkdirPaths = append (mkdirPaths , p .Path )
550+ }
551+ assert .Contains (t , mkdirPaths , "mount://-/boot" )
552+ assert .NotContains (t , mkdirPaths , "mount://boot/efi" )
553+
554+ // Only one pre-install selinux stage (root only, no /boot stage)
555+ preInstallSELinuxStages := findPreInstallStages ("org.osbuild.selinux" , pipeline .Stages )
556+ require .Equal (t , 1 , len (preInstallSELinuxStages ))
557+
558+ rootSELinuxOpts := preInstallSELinuxStages [0 ].Options .(* osbuild.SELinuxStageOptions )
559+ assert .Equal (t , "mount://-/" , rootSELinuxOpts .Target )
560+ }
561+
391562func TestRawBootcPXE (t * testing.T ) {
392563 rawBootcPipeline := makeFakeRawBootcPipeline ()
393564 rawBootcPipeline .KernelVersion = "5.14.0-611.4.1.el9_7.x86_64"
0 commit comments