Skip to content

Commit 5de3906

Browse files
Merge pull request #5318 from umohnani8/reboots
MCO-1529, MCO-1530: Implement install time support for Image Mode
2 parents de97fee + d8f814b commit 5de3906

File tree

14 files changed

+1294
-37
lines changed

14 files changed

+1294
-37
lines changed

pkg/controller/bootstrap/bootstrap.go

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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+
}

pkg/controller/bootstrap/bootstrap_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,66 @@ func TestBootstrapRun(t *testing.T) {
200200
})
201201
}
202202
}
203+
204+
func TestValidatePreBuiltImage(t *testing.T) {
205+
tests := []struct {
206+
name string
207+
imageSpec string
208+
errorContains string
209+
}{
210+
{
211+
name: "Valid image with proper digest format",
212+
imageSpec: "registry.example.com/test@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
213+
errorContains: "",
214+
},
215+
{
216+
name: "Empty image spec should fail",
217+
imageSpec: "",
218+
errorContains: "cannot be empty",
219+
},
220+
{
221+
name: "Image without digest should fail",
222+
imageSpec: "registry.example.com/test:latest",
223+
errorContains: "must use digested format",
224+
},
225+
{
226+
name: "Image with invalid digest length should fail",
227+
imageSpec: "registry.example.com/test@sha256:12345",
228+
errorContains: "invalid reference format",
229+
},
230+
{
231+
name: "Image with invalid digest characters should fail",
232+
imageSpec: "registry.example.com/test@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdez",
233+
errorContains: "invalid reference format",
234+
},
235+
{
236+
name: "Image with uppercase digest should fail",
237+
imageSpec: "registry.example.com/test@sha256:1234567890ABCDEF1234567890abcdef1234567890abcdef1234567890abcdef",
238+
errorContains: "invalid checksum digest format",
239+
},
240+
{
241+
name: "Image with MD5 digest should fail",
242+
imageSpec: "registry.example.com/test@md5:1234567890abcdef1234567890abcdef",
243+
errorContains: "unsupported digest algorithm",
244+
},
245+
}
246+
247+
for _, tt := range tests {
248+
t.Run(tt.name, func(t *testing.T) {
249+
err := validatePreBuiltImage(tt.imageSpec)
250+
251+
if tt.errorContains != "" && err == nil {
252+
t.Errorf("Expected error but got none")
253+
}
254+
if tt.errorContains == "" && err != nil {
255+
t.Errorf("Unexpected error: %v", err)
256+
}
257+
if tt.errorContains != "" {
258+
// If we reach here, err must be non-nil (checked above)
259+
if !strings.Contains(err.Error(), tt.errorContains) {
260+
t.Errorf("Expected error to contain %q, but got: %v", tt.errorContains, err)
261+
}
262+
}
263+
})
264+
}
265+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
apiVersion: machineconfiguration.openshift.io/v1
2+
kind: MachineConfigPool
3+
metadata:
4+
name: layered-worker
5+
spec:
6+
machineConfigSelector:
7+
matchLabels:
8+
"machineconfiguration.openshift.io/role": "layered-worker"
9+
nodeSelector:
10+
matchLabels:
11+
node-role.kubernetes.io/layered-worker: ""
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
apiVersion: machineconfiguration.openshift.io/v1
2+
kind: MachineOSConfig
3+
metadata:
4+
name: layered-worker
5+
annotations:
6+
machineconfiguration.openshift.io/pre-built-image: "quay.io/example/layered-rhcos:latest@sha256:abcd1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab"
7+
spec:
8+
machineConfigPool:
9+
name: layered-worker
10+
imageBuilder:
11+
imageBuilderType: Job
12+
baseImagePullSecret:
13+
name: pull-secret
14+
renderedImagePushSecret:
15+
name: push-secret
16+
renderedImagePushSpec: quay.io/example/layered-rhcos:latest
17+
containerFile:
18+
- containerfileArch: NoArch
19+
content: |
20+
FROM configs AS final
21+
RUN rpm-ostree install httpd && \
22+
ostree container commit

pkg/controller/build/constants/constants.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,18 @@ const (
1313
TargetMachineConfigPoolLabelKey = "machineconfiguration.openshift.io/target-machine-config-pool"
1414
)
1515

16+
// New labels for pre-built image tracking
17+
const (
18+
// PreBuiltImageLabelKey marks MachineOSBuild objects created from pre-built images
19+
PreBuiltImageLabelKey = "machineconfiguration.openshift.io/pre-built-image"
20+
)
21+
22+
// Common label values
23+
const (
24+
// TrueValue is the string representation of true used in labels and annotations
25+
TrueValue = "true"
26+
)
27+
1628
// Annotations added to all ephemeral build objects BuildController creates.
1729
const (
1830
MachineOSBuildNameAnnotationKey = "machineconfiguration.openshift.io/machine-os-build"
@@ -36,6 +48,31 @@ const (
3648
RebuildMachineOSConfigAnnotationKey string = "machineconfiguration.openshift.io/rebuild"
3749
)
3850

51+
// New annotations for pre-built image support
52+
const (
53+
// PreBuiltImageAnnotationKey indicates a MachineOSConfig should be seeded with a pre-built image
54+
PreBuiltImageAnnotationKey = "machineconfiguration.openshift.io/pre-built-image"
55+
)
56+
57+
// MachineOSConfig condition types
58+
// TODO: These should eventually be moved to the API package once MOSC conditions are finalized
59+
const (
60+
// MachineOSConfigSeeded indicates that a MachineOSConfig has been seeded with a pre-built image at install time
61+
MachineOSConfigSeeded = "Seeded"
62+
)
63+
64+
// MachineOSConfig condition reasons
65+
const (
66+
// ReasonPreBuiltImageSeeded indicates the MachineOSConfig was seeded with a pre-built image
67+
ReasonPreBuiltImageSeeded = "PreBuiltImageSeeded"
68+
)
69+
70+
// Component MachineConfig naming for pre-built images
71+
const (
72+
// PreBuiltImageMachineConfigPrefix is the prefix for component MCs that set osImageURL from pre-built images
73+
PreBuiltImageMachineConfigPrefix = "10-prebuiltimage-osimageurl-"
74+
)
75+
3976
// Entitled build secret names
4077
const (
4178
// Name of the etc-pki-entitlement secret from the openshift-config-managed namespace.

pkg/controller/build/helpers.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,43 @@ func hasRebuildAnnotation(mosc *mcfgv1.MachineOSConfig) bool {
256256
return metav1.HasAnnotation(mosc.ObjectMeta, constants.RebuildMachineOSConfigAnnotationKey)
257257
}
258258

259+
// getPreBuiltImage returns the pre-built image from a MachineOSConfig's annotations.
260+
// Returns the image string and a boolean indicating if it exists and is non-empty.
261+
func getPreBuiltImage(mosc *mcfgv1.MachineOSConfig) (string, bool) {
262+
image, exists := mosc.Annotations[constants.PreBuiltImageAnnotationKey]
263+
return image, exists && image != ""
264+
}
265+
266+
// shouldSeedWithPreBuiltImage determines if a MachineOSConfig should be seeded with a pre-built image.
267+
// Returns true if:
268+
// - The MOSC has a pre-built image annotation
269+
// - The MOSC does NOT have a current build annotation (meaning seeding hasn't happened yet)
270+
func shouldSeedWithPreBuiltImage(mosc *mcfgv1.MachineOSConfig) bool {
271+
_, hasImage := getPreBuiltImage(mosc)
272+
return hasImage && !hasCurrentBuildAnnotation(mosc)
273+
}
274+
275+
// isPreBuiltImageAwaitingSeeding checks if a MOSC has pre-built image annotation but hasn't been seeded.
276+
// This is useful for skipping normal build workflows when the seeding workflow should handle it.
277+
// Seeding is considered complete once the currentBuild annotation is set.
278+
func isPreBuiltImageAwaitingSeeding(mosc *mcfgv1.MachineOSConfig) bool {
279+
_, hasImage := getPreBuiltImage(mosc)
280+
return hasImage && !hasCurrentBuildAnnotation(mosc)
281+
}
282+
283+
// needsPreBuiltImageAnnotationCleanup determines if a MOSC has completed seeding and
284+
// the pre-built image annotation can be safely removed.
285+
// Returns true if:
286+
// - The MOSC has a current build annotation (seeding is complete)
287+
// - The MOSC status has been populated with CurrentImagePullSpec
288+
// - The MOSC still has the PreBuiltImageAnnotationKey (needs cleanup)
289+
func needsPreBuiltImageAnnotationCleanup(mosc *mcfgv1.MachineOSConfig) bool {
290+
_, hasImage := getPreBuiltImage(mosc)
291+
return hasCurrentBuildAnnotation(mosc) &&
292+
mosc.Status.CurrentImagePullSpec != "" &&
293+
hasImage
294+
}
295+
259296
// Looks at the error chain for the given error and determines if the error
260297
// should be ignored or not based upon whether it is a not found error. If it
261298
// should be ignored, this will log the error as well as the name and kind of

0 commit comments

Comments
 (0)