@@ -18,10 +18,14 @@ import (
1818 kscheme "k8s.io/client-go/kubernetes/scheme"
1919 "k8s.io/klog/v2"
2020
21+ "github.com/containers/image/v5/docker/reference"
22+ "github.com/opencontainers/go-digest"
2123 apicfgv1 "github.com/openshift/api/config/v1"
2224 apicfgv1alpha1 "github.com/openshift/api/config/v1alpha1"
2325 mcfgv1 "github.com/openshift/api/machineconfiguration/v1"
2426 apioperatorsv1alpha1 "github.com/openshift/api/operator/v1alpha1"
27+ "github.com/openshift/machine-config-operator/pkg/apihelpers"
28+ buildconstants "github.com/openshift/machine-config-operator/pkg/controller/build/constants"
2529 ctrlcommon "github.com/openshift/machine-config-operator/pkg/controller/common"
2630 containerruntimeconfig "github.com/openshift/machine-config-operator/pkg/controller/container-runtime-config"
2731 kubeletconfig "github.com/openshift/machine-config-operator/pkg/controller/kubelet-config"
@@ -73,8 +77,9 @@ func (b *Bootstrap) Run(destDir string) error {
7377 apioperatorsv1alpha1 .Install (scheme )
7478 apicfgv1 .Install (scheme )
7579 apicfgv1alpha1 .Install (scheme )
80+ corev1 .AddToScheme (scheme )
7681 codecFactory := serializer .NewCodecFactory (scheme )
77- decoder := codecFactory .UniversalDecoder (mcfgv1 .GroupVersion , apioperatorsv1alpha1 .GroupVersion , apicfgv1 .GroupVersion , apicfgv1alpha1 .GroupVersion )
82+ decoder := codecFactory .UniversalDecoder (mcfgv1 .GroupVersion , apioperatorsv1alpha1 .GroupVersion , apicfgv1 .GroupVersion , apicfgv1alpha1 .GroupVersion , corev1 . SchemeGroupVersion )
7883
7984 var (
8085 cconfig * mcfgv1.ControllerConfig
@@ -83,6 +88,7 @@ func (b *Bootstrap) Run(destDir string) error {
8388 kconfigs []* mcfgv1.KubeletConfig
8489 pools []* mcfgv1.MachineConfigPool
8590 configs []* mcfgv1.MachineConfig
91+ machineOSConfigs []* mcfgv1.MachineOSConfig
8692 crconfigs []* mcfgv1.ContainerRuntimeConfig
8793 icspRules []* apioperatorsv1alpha1.ImageContentSourcePolicy
8894 idmsRules []* apicfgv1.ImageDigestMirrorSet
@@ -124,6 +130,8 @@ func (b *Bootstrap) Run(destDir string) error {
124130 pools = append (pools , obj )
125131 case * mcfgv1.MachineConfig :
126132 configs = append (configs , obj )
133+ case * mcfgv1.MachineOSConfig :
134+ machineOSConfigs = append (machineOSConfigs , obj )
127135 case * mcfgv1.ControllerConfig :
128136 cconfig = obj
129137 case * mcfgv1.ContainerRuntimeConfig :
@@ -234,6 +242,18 @@ func (b *Bootstrap) Run(destDir string) error {
234242 }
235243 klog .Infof ("Successfully generated MachineConfigs from kubelet configs." )
236244
245+ // Create component MachineConfigs for pre-built images for hybrid OCL
246+ // This must happen BEFORE render.RunBootstrap() so they can be merged into rendered MCs
247+ if len (machineOSConfigs ) > 0 {
248+ klog .Infoln ("Found machineOSConfig(s) at install time, will install cluster with Image Mode enabled" )
249+ preBuiltImageMCs , err := createPreBuiltImageMachineConfigs (machineOSConfigs , pools )
250+ if err != nil {
251+ return fmt .Errorf ("failed to create pre-built image MachineConfigs: %w" , err )
252+ }
253+ configs = append (configs , preBuiltImageMCs ... )
254+ klog .Infof ("Successfully created %d pre-built image component MachineConfigs for hybrid OCL." , len (preBuiltImageMCs ))
255+ }
256+
237257 fpools , gconfigs , err := render .RunBootstrap (pools , configs , cconfig )
238258 if err != nil {
239259 return err
@@ -376,3 +396,101 @@ func parseManifests(filename string, r io.Reader) ([]manifest, error) {
376396 manifests = append (manifests , m )
377397 }
378398}
399+
400+ // createPreBuiltImageMachineConfigs creates component MachineConfigs that set osImageURL for pools
401+ // that have associated MachineOSConfigs with pre-built image annotations.
402+ // These component MCs will be automatically merged into rendered MCs by the render controller.
403+ // This function validates pre-built images at bootstrap time and will fail if:
404+ // - The annotation is missing for a MOSC that hasn't been seeded yet (required for install-time OCL)
405+ // - The image format is invalid
406+ // MOSCs without the annotation are skipped only if seeding has already completed.
407+ func createPreBuiltImageMachineConfigs (machineOSConfigs []* mcfgv1.MachineOSConfig , pools []* mcfgv1.MachineConfigPool ) ([]* mcfgv1.MachineConfig , error ) {
408+ var preBuiltImageMCs []* mcfgv1.MachineConfig
409+ var validationErrors []error
410+
411+ for _ , mosc := range machineOSConfigs {
412+ preBuiltImage , hasPreBuiltImage := mosc .Annotations [buildconstants .PreBuiltImageAnnotationKey ]
413+
414+ // Check if seeding has completed (either Seeded condition or currentBuild annotation)
415+ hasSeededCondition := apihelpers .IsMachineOSConfigConditionPresentAndEqual (mosc .Status .Conditions , buildconstants .MachineOSConfigSeeded , metav1 .ConditionTrue )
416+ hasCurrentBuild := mosc .Annotations [buildconstants .CurrentMachineOSBuildAnnotationKey ] != ""
417+ isPostSeeding := hasSeededCondition || hasCurrentBuild
418+
419+ // Validate annotation presence
420+ if ! hasPreBuiltImage || preBuiltImage == "" {
421+ if isPostSeeding {
422+ // Post-seeding: annotation was removed after seeding completed, this is OK, skip
423+ klog .V (4 ).Infof ("Skipping MachineOSConfig %s - no pre-built image annotation but seeding already completed" , mosc .Name )
424+ continue
425+ }
426+ // Pre-seeding: annotation is REQUIRED for install-time OCL
427+ validationErrors = append (validationErrors , fmt .Errorf ("MachineOSConfig %q is missing required annotation %q - this is required for install-time OCL" ,
428+ mosc .Name , buildconstants .PreBuiltImageAnnotationKey ))
429+ continue
430+ }
431+
432+ poolName := mosc .Spec .MachineConfigPool .Name
433+ // verify that the pool used in the MOSC actually exists
434+ poolExists := false
435+ for _ , pool := range pools {
436+ if pool .Name == poolName {
437+ poolExists = true
438+ break
439+ }
440+ }
441+ if ! poolExists {
442+ validationErrors = append (validationErrors , fmt .Errorf ("MachineOSConfig %q references non-existent pool %q" , mosc .Name , poolName ))
443+ continue
444+ }
445+
446+ // Validate the pre-built image format and digest
447+ if err := validatePreBuiltImage (preBuiltImage ); err != nil {
448+ validationErrors = append (validationErrors , fmt .Errorf ("invalid pre-built image %q for MachineOSConfig %q (pool %q): %w" ,
449+ preBuiltImage , mosc .Name , poolName , err ))
450+ continue
451+ }
452+
453+ // Create the component MachineConfig
454+ mc := ctrlcommon .CreatePreBuiltImageMachineConfig (poolName , preBuiltImage , buildconstants .PreBuiltImageAnnotationKey )
455+ preBuiltImageMCs = append (preBuiltImageMCs , mc )
456+ klog .Infof ("Validated and created component MachineConfig %s with OSImageURL: %s for pool %s" , mc .Name , preBuiltImage , poolName )
457+ }
458+
459+ // Return all validation errors if any occurred
460+ if len (validationErrors ) > 0 {
461+ return nil , fmt .Errorf ("validation failed for MachineOSConfigs: %s" , errors .Join (validationErrors ... ))
462+ }
463+
464+ return preBuiltImageMCs , nil
465+ }
466+
467+ // validatePreBuiltImage validates the pre-built image format using containers/image library
468+ func validatePreBuiltImage (imageSpec string ) error {
469+ if imageSpec == "" {
470+ return fmt .Errorf ("pre-built image spec cannot be empty" )
471+ }
472+
473+ // Use the containers/image library to parse and validate the image reference
474+ ref , err := reference .ParseNamed (imageSpec )
475+ if err != nil {
476+ return fmt .Errorf ("pre-built image %q has invalid format: %w" , imageSpec , err )
477+ }
478+
479+ // Ensure the reference has a digest (is canonical)
480+ canonical , ok := ref .(reference.Canonical )
481+ if ! ok {
482+ return fmt .Errorf ("pre-built image must use digested format (image@sha256:digest), got: %q" , imageSpec )
483+ }
484+
485+ // Validate the digest using the go-digest library
486+ if err := canonical .Digest ().Validate (); err != nil {
487+ return fmt .Errorf ("pre-built image %q has invalid digest: %w" , imageSpec , err )
488+ }
489+
490+ // Ensure it's specifically a SHA256 digest (which is what we expect for container images)
491+ if canonical .Digest ().Algorithm () != digest .SHA256 {
492+ return fmt .Errorf ("pre-built image must use SHA256 digest, got %s: %q" , canonical .Digest ().Algorithm (), imageSpec )
493+ }
494+
495+ return nil
496+ }
0 commit comments