Skip to content

Commit 0c4d515

Browse files
committed
Implement install time support for Image Mode
Add install time support for Image Mode by using a pre-built custom OS container image with the MachineOSConfig CR created at install time in the manifests directory. **Core Workflow:** 1. Bootstrap: Creates component MachineConfig (10-prebuiltimage-osimageurl-<pool>) that sets osImageURL from MOSC annotation 2. Cluster startup: Seeding workflow creates synthetic MachineOSBuild with success status 3. Post-install: Normal OCL workflows handle MC changes and image updates **Seeding Implementation:** - Annotation-driven detection in addMachineOSConfig() - seedMachineOSConfigWithExistingImage() orchestrates seeding workflow - createSyntheticMachineOSBuild() generates synthetic MOSB marked as successful - updateMachineOSConfigForSeeding() updates MOSC status and annotations - Adds "Seeded" condition and PreBuiltImageSeededAnnotationKey marker - PreBuiltImageAnnotationKey remains on MOSC for component MC management **Bootstrap Integration:** - Recognizes MachineOSConfig manifests during bootstrap processing - Creates component MachineConfigs that set osImageURL for each pool - Component MCs are merged into rendered MCs by render controller - Validates pre-built image format (requires digest @sha256:) **Operator Sync:** - syncPreBuiltImageMachineConfigs() manages component MC lifecycle - Creates/updates component MCs based on MOSC annotations - Deletes component MCs when annotation is removed from MOSC **Install-Time Guards:** - Handles empty MCP status.configuration.name during pool convergence - Skips rendered config comparison when status.configuration.name is empty - Prevents MachineConfig lookups with empty names at install time - Node controller uses current-machine-os-build annotation for MOSB lookup **Key Components:** - PreBuiltImageAnnotationKey: Triggers seeding, persists for component MC management - PreBuiltImageSeededAnnotationKey: Tracks seeding completion - PreBuiltImageLabelKey: Labels synthetic MachineOSBuilds - "Seeded" condition: Status indicator for successfully seeded MOSCs **Testing:** - Comprehensive unit tests for seeding workflow - Bootstrap test validation with example manifest - Tests for annotation detection and routing logic Signed-off-by: Urvashi <umohnani@redhat.com>
1 parent 8dc0f4d commit 0c4d515

File tree

14 files changed

+1103
-38
lines changed

14 files changed

+1103
-38
lines changed

pkg/controller/bootstrap/bootstrap.go

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@ 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+
buildconstants "github.com/openshift/machine-config-operator/pkg/controller/build/constants"
2528
ctrlcommon "github.com/openshift/machine-config-operator/pkg/controller/common"
2629
containerruntimeconfig "github.com/openshift/machine-config-operator/pkg/controller/container-runtime-config"
2730
kubeletconfig "github.com/openshift/machine-config-operator/pkg/controller/kubelet-config"
@@ -73,8 +76,9 @@ func (b *Bootstrap) Run(destDir string) error {
7376
apioperatorsv1alpha1.Install(scheme)
7477
apicfgv1.Install(scheme)
7578
apicfgv1alpha1.Install(scheme)
79+
corev1.AddToScheme(scheme)
7680
codecFactory := serializer.NewCodecFactory(scheme)
77-
decoder := codecFactory.UniversalDecoder(mcfgv1.GroupVersion, apioperatorsv1alpha1.GroupVersion, apicfgv1.GroupVersion, apicfgv1alpha1.GroupVersion)
81+
decoder := codecFactory.UniversalDecoder(mcfgv1.GroupVersion, apioperatorsv1alpha1.GroupVersion, apicfgv1.GroupVersion, apicfgv1alpha1.GroupVersion, corev1.SchemeGroupVersion)
7882

7983
var (
8084
cconfig *mcfgv1.ControllerConfig
@@ -83,6 +87,7 @@ func (b *Bootstrap) Run(destDir string) error {
8387
kconfigs []*mcfgv1.KubeletConfig
8488
pools []*mcfgv1.MachineConfigPool
8589
configs []*mcfgv1.MachineConfig
90+
machineOSConfigs []*mcfgv1.MachineOSConfig
8691
crconfigs []*mcfgv1.ContainerRuntimeConfig
8792
icspRules []*apioperatorsv1alpha1.ImageContentSourcePolicy
8893
idmsRules []*apicfgv1.ImageDigestMirrorSet
@@ -124,6 +129,8 @@ func (b *Bootstrap) Run(destDir string) error {
124129
pools = append(pools, obj)
125130
case *mcfgv1.MachineConfig:
126131
configs = append(configs, obj)
132+
case *mcfgv1.MachineOSConfig:
133+
machineOSConfigs = append(machineOSConfigs, obj)
127134
case *mcfgv1.ControllerConfig:
128135
cconfig = obj
129136
case *mcfgv1.ContainerRuntimeConfig:
@@ -234,6 +241,17 @@ func (b *Bootstrap) Run(destDir string) error {
234241
}
235242
klog.Infof("Successfully generated MachineConfigs from kubelet configs.")
236243

244+
// Create component MachineConfigs for pre-built images for hybrid OCL
245+
// This must happen BEFORE render.RunBootstrap() so they can be merged into rendered MCs
246+
if len(machineOSConfigs) > 0 {
247+
preBuiltImageMCs, err := createPreBuiltImageMachineConfigs(machineOSConfigs, pools)
248+
if err != nil {
249+
return fmt.Errorf("failed to create pre-built image MachineConfigs: %w", err)
250+
}
251+
configs = append(configs, preBuiltImageMCs...)
252+
klog.Infof("Successfully created %d pre-built image component MachineConfigs for hybrid OCL.", len(preBuiltImageMCs))
253+
}
254+
237255
fpools, gconfigs, err := render.RunBootstrap(pools, configs, cconfig)
238256
if err != nil {
239257
return err
@@ -376,3 +394,69 @@ func parseManifests(filename string, r io.Reader) ([]manifest, error) {
376394
manifests = append(manifests, m)
377395
}
378396
}
397+
398+
// createPreBuiltImageMachineConfigs creates component MachineConfigs that set osImageURL for pools
399+
// that have associated MachineOSConfigs with pre-built image annotations.
400+
// These component MCs will be automatically merged into rendered MCs by the render controller.
401+
// This function performs strict validation at bootstrap time and will fail if:
402+
// - A MachineOSConfig is missing the pre-built image annotation
403+
// - The pre-built image format or digest is invalid
404+
func createPreBuiltImageMachineConfigs(machineOSConfigs []*mcfgv1.MachineOSConfig, pools []*mcfgv1.MachineConfigPool) ([]*mcfgv1.MachineConfig, error) {
405+
var preBuiltImageMCs []*mcfgv1.MachineConfig
406+
407+
// At bootstrap time, we require ALL MachineOSConfigs to have pre-built images
408+
// This is a strict requirement for day-0 hybrid OCL support
409+
for _, mosc := range machineOSConfigs {
410+
preBuiltImage, hasPreBuiltImage := mosc.Annotations[buildconstants.PreBuiltImageAnnotationKey]
411+
if !hasPreBuiltImage || preBuiltImage == "" {
412+
return nil, fmt.Errorf("MachineOSConfig %s is missing required annotation %s for bootstrap pre-built image support",
413+
mosc.Name, buildconstants.PreBuiltImageAnnotationKey)
414+
}
415+
416+
poolName := mosc.Spec.MachineConfigPool.Name
417+
418+
// Validate the pre-built image format and digest
419+
if err := validatePreBuiltImage(preBuiltImage); err != nil {
420+
return nil, fmt.Errorf("invalid pre-built image %q for MachineOSConfig %s (pool %s): %w",
421+
preBuiltImage, mosc.Name, poolName, err)
422+
}
423+
424+
// Create the component MachineConfig
425+
mc := ctrlcommon.CreatePreBuiltImageMachineConfig(poolName, preBuiltImage, buildconstants.PreBuiltImageAnnotationKey)
426+
preBuiltImageMCs = append(preBuiltImageMCs, mc)
427+
klog.Infof("✓ Validated and created component MachineConfig %s with OSImageURL: %s for pool %s", mc.Name, preBuiltImage, poolName)
428+
}
429+
430+
return preBuiltImageMCs, nil
431+
}
432+
433+
// validatePreBuiltImage validates the pre-built image format using containers/image library
434+
func validatePreBuiltImage(imageSpec string) error {
435+
if imageSpec == "" {
436+
return fmt.Errorf("pre-built image spec cannot be empty")
437+
}
438+
439+
// Use the containers/image library to parse and validate the image reference
440+
ref, err := reference.ParseNamed(imageSpec)
441+
if err != nil {
442+
return fmt.Errorf("pre-built image has invalid format: %w", err)
443+
}
444+
445+
// Ensure the reference has a digest (is canonical)
446+
canonical, ok := ref.(reference.Canonical)
447+
if !ok {
448+
return fmt.Errorf("pre-built image must use digested format (image@sha256:digest), got: %q", imageSpec)
449+
}
450+
451+
// Validate the digest using the go-digest library
452+
if err := canonical.Digest().Validate(); err != nil {
453+
return fmt.Errorf("pre-built image has invalid digest: %w", err)
454+
}
455+
456+
// Ensure it's specifically a SHA256 digest (which is what we expect for container images)
457+
if canonical.Digest().Algorithm() != digest.SHA256 {
458+
return fmt.Errorf("pre-built image must use SHA256 digest, got %s: %q", canonical.Digest().Algorithm(), imageSpec)
459+
}
460+
461+
return nil
462+
}

pkg/controller/bootstrap/bootstrap_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,72 @@ 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+
expectedError bool
209+
errorContains string
210+
}{
211+
{
212+
name: "Valid image with proper digest format",
213+
imageSpec: "registry.example.com/test@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
214+
expectedError: false,
215+
},
216+
{
217+
name: "Empty image spec should fail",
218+
imageSpec: "",
219+
expectedError: true,
220+
errorContains: "cannot be empty",
221+
},
222+
{
223+
name: "Image without digest should fail",
224+
imageSpec: "registry.example.com/test:latest",
225+
expectedError: true,
226+
errorContains: "must use digested format",
227+
},
228+
{
229+
name: "Image with invalid digest length should fail",
230+
imageSpec: "registry.example.com/test@sha256:12345",
231+
expectedError: true,
232+
errorContains: "invalid reference format",
233+
},
234+
{
235+
name: "Image with invalid digest characters should fail",
236+
imageSpec: "registry.example.com/test@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdez",
237+
expectedError: true,
238+
errorContains: "invalid reference format",
239+
},
240+
{
241+
name: "Image with uppercase digest should fail",
242+
imageSpec: "registry.example.com/test@sha256:1234567890ABCDEF1234567890abcdef1234567890abcdef1234567890abcdef",
243+
expectedError: true,
244+
errorContains: "invalid checksum digest format",
245+
},
246+
{
247+
name: "Image with MD5 digest should fail",
248+
imageSpec: "registry.example.com/test@md5:1234567890abcdef1234567890abcdef",
249+
expectedError: true,
250+
errorContains: "unsupported digest algorithm",
251+
},
252+
}
253+
254+
for _, tt := range tests {
255+
t.Run(tt.name, func(t *testing.T) {
256+
err := validatePreBuiltImage(tt.imageSpec)
257+
258+
if tt.expectedError && err == nil {
259+
t.Errorf("Expected error but got none")
260+
}
261+
if !tt.expectedError && err != nil {
262+
t.Errorf("Unexpected error: %v", err)
263+
}
264+
if tt.expectedError && err != nil && tt.errorContains != "" {
265+
if !strings.Contains(err.Error(), tt.errorContains) {
266+
t.Errorf("Expected error to contain %q, but got: %v", tt.errorContains, err)
267+
}
268+
}
269+
})
270+
}
271+
}
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: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ 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+
1622
// Annotations added to all ephemeral build objects BuildController creates.
1723
const (
1824
MachineOSBuildNameAnnotationKey = "machineconfiguration.openshift.io/machine-os-build"
@@ -36,6 +42,20 @@ const (
3642
RebuildMachineOSConfigAnnotationKey string = "machineconfiguration.openshift.io/rebuild"
3743
)
3844

45+
// New annotations for pre-built image support
46+
const (
47+
// PreBuiltImageAnnotationKey indicates a MachineOSConfig should be seeded with a pre-built image
48+
PreBuiltImageAnnotationKey = "machineconfiguration.openshift.io/pre-built-image"
49+
// PreBuiltImageSeededAnnotationKey indicates that the initial synthetic MOSB has been created for this MOSC
50+
PreBuiltImageSeededAnnotationKey = "machineconfiguration.openshift.io/pre-built-image-seeded"
51+
)
52+
53+
// Component MachineConfig naming for pre-built images
54+
const (
55+
// PreBuiltImageMachineConfigPrefix is the prefix for component MCs that set osImageURL from pre-built images
56+
PreBuiltImageMachineConfigPrefix = "10-prebuiltimage-osimageurl-"
57+
)
58+
3959
// Entitled build secret names
4060
const (
4161
// Name of the etc-pki-entitlement secret from the openshift-config-managed namespace.

pkg/controller/build/helpers.go

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

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

0 commit comments

Comments
 (0)