@@ -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,129 @@ 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 stage should create /boot and /boot/efi directories
448+ preInstallMkdirStages := findPreInstallStages ("org.osbuild.mkdir" , pipeline .Stages )
449+ require .Equal (t , 1 , len (preInstallMkdirStages ))
450+ mkdirOpts := preInstallMkdirStages [0 ].Options .(* osbuild.MkdirStageOptions )
451+ var mkdirPaths []string
452+ for _ , p := range mkdirOpts .Paths {
453+ mkdirPaths = append (mkdirPaths , p .Path )
454+ }
455+ assert .Contains (t , mkdirPaths , "mount://-/boot" )
456+ assert .Contains (t , mkdirPaths , "mount://boot/efi" )
457+ // mkdir stage should have devices and mounts (root + boot)
458+ assert .NotEmpty (t , preInstallMkdirStages [0 ].Devices )
459+ assert .NotEmpty (t , preInstallMkdirStages [0 ].Mounts )
460+
461+ // Pre-install selinux stages: one for root, one for /boot
462+ preInstallSELinuxStages := findPreInstallStages ("org.osbuild.selinux" , pipeline .Stages )
463+ require .Equal (t , 2 , len (preInstallSELinuxStages ))
464+
465+ // First SELinux stage: labels root mount (without boot mounted)
466+ rootSELinuxOpts := preInstallSELinuxStages [0 ].Options .(* osbuild.SELinuxStageOptions )
467+ assert .Equal (t , "mount://-/" , rootSELinuxOpts .Target )
468+ assert .Contains (t , rootSELinuxOpts .FileContexts , "input://tree/etc/selinux/targeted/contexts/files/file_contexts" )
469+ // Should have inputs (tree input from source pipeline)
470+ assert .NotNil (t , preInstallSELinuxStages [0 ].Inputs )
471+ // Should only mount root
472+ rootMountTargets := make ([]string , 0 )
473+ for _ , mnt := range preInstallSELinuxStages [0 ].Mounts {
474+ rootMountTargets = append (rootMountTargets , mnt .Target )
475+ }
476+ assert .Contains (t , rootMountTargets , "/" )
477+ assert .NotContains (t , rootMountTargets , "/boot" )
478+
479+ // Second SELinux stage: labels /boot (with boot mounted)
480+ bootSELinuxOpts := preInstallSELinuxStages [1 ].Options .(* osbuild.SELinuxStageOptions )
481+ assert .Equal (t , "mount://-/boot/" , bootSELinuxOpts .Target )
482+ assert .Contains (t , bootSELinuxOpts .FileContexts , "input://tree/etc/selinux/targeted/contexts/files/file_contexts" )
483+ assert .NotNil (t , preInstallSELinuxStages [1 ].Inputs )
484+ // Should mount both root and boot
485+ bootMountTargets := make ([]string , 0 )
486+ for _ , mnt := range preInstallSELinuxStages [1 ].Mounts {
487+ bootMountTargets = append (bootMountTargets , mnt .Target )
488+ }
489+ assert .Contains (t , bootMountTargets , "/" )
490+ assert .Contains (t , bootMountTargets , "/boot" )
491+ }
492+
493+ func TestRawBootcImageSerializeMountpointSELinuxLabelingNoSELinux (t * testing.T ) {
494+ rawBootcPipeline := makeFakeRawBootcPipeline ()
495+ // No SELinux set - no pre-install labeling stages should be generated
496+ rawBootcPipeline .OSCustomizations .SELinux = ""
497+
498+ pipeline , err := rawBootcPipeline .Serialize ()
499+ require .NoError (t , err )
500+
501+ preInstallSELinuxStages := findPreInstallStages ("org.osbuild.selinux" , pipeline .Stages )
502+ assert .Equal (t , 0 , len (preInstallSELinuxStages ))
503+ preInstallMkdirStages := findPreInstallStages ("org.osbuild.mkdir" , pipeline .Stages )
504+ assert .Equal (t , 0 , len (preInstallMkdirStages ))
505+ }
506+
507+ func TestRawBootcImageSerializeMountpointSELinuxLabelingNoBoot (t * testing.T ) {
508+ // Test the edge case where there is no separate /boot partition
509+ // (only root + EFI). Only the root SELinux stage should be generated,
510+ // no /boot labeling stage.
511+ mani := manifest .New ()
512+ r := & runner.Linux {}
513+ pf := & platform.Data {
514+ Arch : arch .ARCH_X86_64 ,
515+ UEFIVendor : "test" ,
516+ }
517+ build := manifest .NewBuildFromContainer (& mani , r , nil , nil )
518+ rawBootcPipeline := manifest .NewRawBootcImage (build , nil , pf )
519+ rawBootcPipeline .PartitionTable = testdisk .MakeFakePartitionTable ("/" , "/boot/efi" )
520+ err := rawBootcPipeline .SerializeStart (manifest.Inputs {Containers : []container.Spec {{Source : "foo" }}})
521+ require .NoError (t , err )
522+
523+ rawBootcPipeline .OSCustomizations .SELinux = "targeted"
524+
525+ pipeline , err := rawBootcPipeline .Serialize ()
526+ require .NoError (t , err )
527+
528+ // Pre-install mkdir stage should only create /boot (no /boot/efi since
529+ // there's no separate boot partition to mount it on)
530+ preInstallMkdirStages := findPreInstallStages ("org.osbuild.mkdir" , pipeline .Stages )
531+ require .Equal (t , 1 , len (preInstallMkdirStages ))
532+ mkdirOpts := preInstallMkdirStages [0 ].Options .(* osbuild.MkdirStageOptions )
533+ var mkdirPaths []string
534+ for _ , p := range mkdirOpts .Paths {
535+ mkdirPaths = append (mkdirPaths , p .Path )
536+ }
537+ assert .Contains (t , mkdirPaths , "mount://-/boot" )
538+ assert .NotContains (t , mkdirPaths , "mount://boot/efi" )
539+
540+ // Only one pre-install selinux stage (root only, no /boot stage)
541+ preInstallSELinuxStages := findPreInstallStages ("org.osbuild.selinux" , pipeline .Stages )
542+ require .Equal (t , 1 , len (preInstallSELinuxStages ))
543+
544+ rootSELinuxOpts := preInstallSELinuxStages [0 ].Options .(* osbuild.SELinuxStageOptions )
545+ assert .Equal (t , "mount://-/" , rootSELinuxOpts .Target )
546+ }
547+
391548func TestRawBootcPXE (t * testing.T ) {
392549 rawBootcPipeline := makeFakeRawBootcPipeline ()
393550 rawBootcPipeline .KernelVersion = "5.14.0-611.4.1.el9_7.x86_64"
0 commit comments