Skip to content

Commit 7b4e49b

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 Seeding Implementation for Pre-built Container Image:** - Added annotation-driven seeding detection in addMachineOSConfig() - Implemented seedMachineOSConfigWithExistingImage() for complete seeding workflow - Added createSyntheticMachineOSBuild() to generate "successful" builds for pre-built images - Added updateMachineOSConfigForSeeding() to update status and annotations **Bootstrap Integration:** - Added MachineOSConfig recognition in bootstrap processing - Implemented syncMachineOSConfigs() in operator for post-bootstrap processing - Creates /etc/mcs/bootstrap/machine-os-configs/ directory structure - Processes MachineOSConfig manifests during cluster startup **Constants and Configuration:** - Added PreBuiltImageAnnotationKey constant for annotation-driven behavior - Added bootstrap integration constants and paths - Enhanced build controller with seeding detection logic **Testing Infrastructure:** - Added comprehensive unit tests for seeding workflow - TestCreateSyntheticMachineOSBuild validates synthetic build creation - TestAddMachineOSConfigRouting tests annotation detection - TestAddMachineOSConfigSeeding tests seeding decision logic - Added bootstrap test validation with layered-worker example manifest **Key Features:** - Maintains 100% backward compatibility with existing MachineOSConfig objects - Routes annotated configs to seeding workflow, non-annotated to normal OCL - Creates proper metadata, labels, and object references expected by MCO - Enables seamless integration with existing controller logic **Technical Details:** - Uses PreBuiltImageAnnotationKey annotation to trigger seeding behavior - Validates image format (digest format @sha256: required) - Creates synthetic MachineOSBuild objects with success status - Updates MachineOSConfig status with CurrentImagePullSpec for MCD consumption - Uses PreBuiltImageLabelKey label to keep track of MOSB created by pre-built container image - Integrates with existing sync pipeline and bootstrap completion detection Signed-off-by: Urvashi <[email protected]>
1 parent 0634552 commit 7b4e49b

File tree

11 files changed

+1168
-9
lines changed

11 files changed

+1168
-9
lines changed

pkg/controller/bootstrap/bootstrap.go

Lines changed: 165 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import (
2222
apicfgv1alpha1 "github.com/openshift/api/config/v1alpha1"
2323
mcfgv1 "github.com/openshift/api/machineconfiguration/v1"
2424
apioperatorsv1alpha1 "github.com/openshift/api/operator/v1alpha1"
25+
buildconstants "github.com/openshift/machine-config-operator/pkg/controller/build/constants"
26+
"github.com/containers/image/v5/docker/reference"
27+
"github.com/opencontainers/go-digest"
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
@@ -91,6 +96,7 @@ func (b *Bootstrap) Run(destDir string) error {
9196
imagePolicies []*apicfgv1.ImagePolicy
9297
imgCfg *apicfgv1.Image
9398
apiServer *apicfgv1.APIServer
99+
secrets []*corev1.Secret
94100
)
95101
for _, info := range infos {
96102
if info.IsDir() {
@@ -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:
@@ -154,6 +162,8 @@ func (b *Bootstrap) Run(destDir string) error {
154162
if obj.GetName() == ctrlcommon.APIServerInstanceName {
155163
apiServer = obj
156164
}
165+
case *corev1.Secret:
166+
secrets = append(secrets, obj)
157167
default:
158168
klog.Infof("skipping %q [%d] manifest because of unhandled %T", file.Name(), idx+1, obji)
159169
}
@@ -239,6 +249,14 @@ func (b *Bootstrap) Run(destDir string) error {
239249
return err
240250
}
241251

252+
// Apply pre-built images to rendered MachineConfigs for hybrid OCL
253+
if len(machineOSConfigs) > 0 {
254+
if err := applyPreBuiltImagesToRenderedConfigs(gconfigs, machineOSConfigs, fpools); err != nil {
255+
return err
256+
}
257+
klog.Infof("Successfully applied pre-built images to rendered MachineConfigs for hybrid OCL.")
258+
}
259+
242260
serializer := json.NewYAMLSerializer(json.DefaultMetaFactory, scheme, scheme)
243261
encoder := codecFactory.EncoderForVersion(serializer, mcfgv1.GroupVersion)
244262

@@ -280,6 +298,55 @@ func (b *Bootstrap) Run(destDir string) error {
280298
}
281299
}
282300

301+
// Write MachineOSConfigs to machine-os-configs directory
302+
// These will be created by the MCO controller after cluster startup
303+
if len(machineOSConfigs) > 0 {
304+
mosconfigdir := filepath.Join(destDir, "machine-os-configs")
305+
if err := os.MkdirAll(mosconfigdir, 0o764); err != nil {
306+
return err
307+
}
308+
for _, mosc := range machineOSConfigs {
309+
buf := bytes.Buffer{}
310+
err := encoder.Encode(mosc, &buf)
311+
if err != nil {
312+
return err
313+
}
314+
path := filepath.Join(mosconfigdir, fmt.Sprintf("%s.yaml", mosc.Name))
315+
// Disable gosec here to avoid throwing
316+
// G306: Expect WriteFile permissions to be 0600 or less
317+
// #nosec
318+
if err := os.WriteFile(path, buf.Bytes(), 0o664); err != nil {
319+
return err
320+
}
321+
}
322+
klog.Infof("Successfully wrote %d MachineOSConfig manifests for post-bootstrap creation", len(machineOSConfigs))
323+
}
324+
325+
// Write Secrets to secrets directory
326+
// These will be created by the MCO controller after cluster startup
327+
if len(secrets) > 0 {
328+
secretsdir := filepath.Join(destDir, "secrets")
329+
if err := os.MkdirAll(secretsdir, 0o764); err != nil {
330+
return err
331+
}
332+
secretEncoder := codecFactory.EncoderForVersion(serializer, corev1.SchemeGroupVersion)
333+
for _, secret := range secrets {
334+
buf := bytes.Buffer{}
335+
err := secretEncoder.Encode(secret, &buf)
336+
if err != nil {
337+
return err
338+
}
339+
path := filepath.Join(secretsdir, fmt.Sprintf("%s.yaml", secret.Name))
340+
// Disable gosec here to avoid throwing
341+
// G306: Expect WriteFile permissions to be 0600 or less
342+
// #nosec
343+
if err := os.WriteFile(path, buf.Bytes(), 0o664); err != nil {
344+
return err
345+
}
346+
}
347+
klog.Infof("Successfully wrote %d Secret manifests for post-bootstrap creation", len(secrets))
348+
}
349+
283350
// If an apiServer object exists, write it to /etc/mcs/bootstrap/api-server/api-server.yaml
284351
// so that bootstrap MCS can consume it
285352
if apiServer != nil {
@@ -376,3 +443,100 @@ func parseManifests(filename string, r io.Reader) ([]manifest, error) {
376443
manifests = append(manifests, m)
377444
}
378445
}
446+
447+
// applyPreBuiltImagesToRenderedConfigs applies pre-built images to rendered MachineConfigs
448+
// and annotates MachineConfigPools for pools that have associated MachineOSConfigs with pre-built image annotations
449+
func applyPreBuiltImagesToRenderedConfigs(renderedConfigs []*mcfgv1.MachineConfig, machineOSConfigs []*mcfgv1.MachineOSConfig, pools []*mcfgv1.MachineConfigPool) error {
450+
// Create a map of pool names to pre-built images
451+
poolToPreBuiltImage := make(map[string]string)
452+
453+
for _, mosc := range machineOSConfigs {
454+
// Check if this MachineOSConfig has a pre-built image annotation
455+
preBuiltImage, hasPreBuiltImage := mosc.Annotations[buildconstants.PreBuiltImageAnnotationKey]
456+
if !hasPreBuiltImage || preBuiltImage == "" {
457+
continue
458+
}
459+
460+
// Validate the pre-built image before proceeding
461+
if err := validatePreBuiltImage(preBuiltImage); err != nil {
462+
return fmt.Errorf("invalid pre-built image %q for MachineOSConfig %s: %w", preBuiltImage, mosc.Name, err)
463+
}
464+
465+
klog.Infof("Found MachineOSConfig %s with pre-built image: %s for pool %s", mosc.Name, preBuiltImage, mosc.Spec.MachineConfigPool.Name)
466+
poolToPreBuiltImage[mosc.Spec.MachineConfigPool.Name] = preBuiltImage
467+
}
468+
469+
// Annotate MachineConfigPools with pre-built images
470+
for _, pool := range pools {
471+
if preBuiltImage, exists := poolToPreBuiltImage[pool.Name]; exists {
472+
klog.Infof("Annotating MachineConfigPool %s with pre-built image: %s", pool.Name, preBuiltImage)
473+
if pool.Annotations == nil {
474+
pool.Annotations = make(map[string]string)
475+
}
476+
pool.Annotations[buildconstants.PreBuiltImageAnnotationKey] = preBuiltImage
477+
}
478+
}
479+
480+
// Apply pre-built images to rendered MachineConfigs
481+
for _, mc := range renderedConfigs {
482+
// Get the pool name from the MachineConfig's owner references
483+
var poolName string
484+
for _, ownerRef := range mc.GetOwnerReferences() {
485+
if ownerRef.Kind == "MachineConfigPool" {
486+
poolName = ownerRef.Name
487+
break
488+
}
489+
}
490+
491+
if poolName == "" {
492+
// Skip if we can't determine the pool name
493+
continue
494+
}
495+
496+
// Check if this pool has a pre-built image
497+
if preBuiltImage, exists := poolToPreBuiltImage[poolName]; exists {
498+
klog.Infof("Setting OSImageURL to %s for rendered MachineConfig %s (pool: %s)", preBuiltImage, mc.Name, poolName)
499+
mc.Spec.OSImageURL = preBuiltImage
500+
501+
// Add annotation to track that this MC uses a pre-built image
502+
if mc.Annotations == nil {
503+
mc.Annotations = make(map[string]string)
504+
}
505+
mc.Annotations[buildconstants.PreBuiltImageAnnotationKey] = preBuiltImage
506+
}
507+
}
508+
509+
return nil
510+
}
511+
512+
// validatePreBuiltImage validates the pre-built image format using containers/image library
513+
func validatePreBuiltImage(imageSpec string) error {
514+
if imageSpec == "" {
515+
return fmt.Errorf("pre-built image spec cannot be empty")
516+
}
517+
518+
// Use the containers/image library to parse and validate the image reference
519+
ref, err := reference.ParseNamed(imageSpec)
520+
if err != nil {
521+
return fmt.Errorf("pre-built image has invalid format: %w", err)
522+
}
523+
524+
// Ensure the reference has a digest (is canonical)
525+
canonical, ok := ref.(reference.Canonical)
526+
if !ok {
527+
return fmt.Errorf("pre-built image must use digested format (image@sha256:digest), got: %q", imageSpec)
528+
}
529+
530+
// Validate the digest using the go-digest library
531+
if err := canonical.Digest().Validate(); err != nil {
532+
return fmt.Errorf("pre-built image has invalid digest: %w", err)
533+
}
534+
535+
// Ensure it's specifically a SHA256 digest (which is what we expect for container images)
536+
if canonical.Digest().Algorithm() != digest.SHA256 {
537+
return fmt.Errorf("pre-built image must use SHA256 digest, got %s: %q", canonical.Digest().Algorithm(), imageSpec)
538+
}
539+
540+
return nil
541+
}
542+

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: 14 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,14 @@ 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+
3953
// Entitled build secret names
4054
const (
4155
// Name of the etc-pki-entitlement secret from the openshift-config-managed namespace.

0 commit comments

Comments
 (0)