Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 86 additions & 1 deletion pkg/controller/bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@ import (
kscheme "k8s.io/client-go/kubernetes/scheme"
"k8s.io/klog/v2"

"github.com/containers/image/v5/docker/reference"
"github.com/opencontainers/go-digest"
apicfgv1 "github.com/openshift/api/config/v1"
apicfgv1alpha1 "github.com/openshift/api/config/v1alpha1"
mcfgv1 "github.com/openshift/api/machineconfiguration/v1"
apioperatorsv1alpha1 "github.com/openshift/api/operator/v1alpha1"
buildconstants "github.com/openshift/machine-config-operator/pkg/controller/build/constants"
ctrlcommon "github.com/openshift/machine-config-operator/pkg/controller/common"
containerruntimeconfig "github.com/openshift/machine-config-operator/pkg/controller/container-runtime-config"
kubeletconfig "github.com/openshift/machine-config-operator/pkg/controller/kubelet-config"
Expand Down Expand Up @@ -73,8 +76,9 @@ func (b *Bootstrap) Run(destDir string) error {
apioperatorsv1alpha1.Install(scheme)
apicfgv1.Install(scheme)
apicfgv1alpha1.Install(scheme)
corev1.AddToScheme(scheme)
codecFactory := serializer.NewCodecFactory(scheme)
decoder := codecFactory.UniversalDecoder(mcfgv1.GroupVersion, apioperatorsv1alpha1.GroupVersion, apicfgv1.GroupVersion, apicfgv1alpha1.GroupVersion)
decoder := codecFactory.UniversalDecoder(mcfgv1.GroupVersion, apioperatorsv1alpha1.GroupVersion, apicfgv1.GroupVersion, apicfgv1alpha1.GroupVersion, corev1.SchemeGroupVersion)

var (
cconfig *mcfgv1.ControllerConfig
Expand All @@ -83,6 +87,7 @@ func (b *Bootstrap) Run(destDir string) error {
kconfigs []*mcfgv1.KubeletConfig
pools []*mcfgv1.MachineConfigPool
configs []*mcfgv1.MachineConfig
machineOSConfigs []*mcfgv1.MachineOSConfig
crconfigs []*mcfgv1.ContainerRuntimeConfig
icspRules []*apioperatorsv1alpha1.ImageContentSourcePolicy
idmsRules []*apicfgv1.ImageDigestMirrorSet
Expand Down Expand Up @@ -124,6 +129,8 @@ func (b *Bootstrap) Run(destDir string) error {
pools = append(pools, obj)
case *mcfgv1.MachineConfig:
configs = append(configs, obj)
case *mcfgv1.MachineOSConfig:
machineOSConfigs = append(machineOSConfigs, obj)
case *mcfgv1.ControllerConfig:
cconfig = obj
case *mcfgv1.ContainerRuntimeConfig:
Expand Down Expand Up @@ -234,6 +241,17 @@ func (b *Bootstrap) Run(destDir string) error {
}
klog.Infof("Successfully generated MachineConfigs from kubelet configs.")

// Create component MachineConfigs for pre-built images for hybrid OCL
// This must happen BEFORE render.RunBootstrap() so they can be merged into rendered MCs
if len(machineOSConfigs) > 0 {
preBuiltImageMCs, err := createPreBuiltImageMachineConfigs(machineOSConfigs, pools)
if err != nil {
return fmt.Errorf("failed to create pre-built image MachineConfigs: %w", err)
}
configs = append(configs, preBuiltImageMCs...)
klog.Infof("Successfully created %d pre-built image component MachineConfigs for hybrid OCL.", len(preBuiltImageMCs))
}

fpools, gconfigs, err := render.RunBootstrap(pools, configs, cconfig)
if err != nil {
return err
Expand Down Expand Up @@ -280,6 +298,7 @@ func (b *Bootstrap) Run(destDir string) error {
}
}


// If an apiServer object exists, write it to /etc/mcs/bootstrap/api-server/api-server.yaml
// so that bootstrap MCS can consume it
if apiServer != nil {
Expand Down Expand Up @@ -376,3 +395,69 @@ func parseManifests(filename string, r io.Reader) ([]manifest, error) {
manifests = append(manifests, m)
}
}

// createPreBuiltImageMachineConfigs creates component MachineConfigs that set osImageURL for pools
// that have associated MachineOSConfigs with pre-built image annotations.
// These component MCs will be automatically merged into rendered MCs by the render controller.
// This function performs strict validation at bootstrap time and will fail if:
// - A MachineOSConfig is missing the pre-built image annotation
// - The pre-built image format or digest is invalid
func createPreBuiltImageMachineConfigs(machineOSConfigs []*mcfgv1.MachineOSConfig, pools []*mcfgv1.MachineConfigPool) ([]*mcfgv1.MachineConfig, error) {
var preBuiltImageMCs []*mcfgv1.MachineConfig

// At bootstrap time, we require ALL MachineOSConfigs to have pre-built images
// This is a strict requirement for day-0 hybrid OCL support
for _, mosc := range machineOSConfigs {
preBuiltImage, hasPreBuiltImage := mosc.Annotations[buildconstants.PreBuiltImageAnnotationKey]
if !hasPreBuiltImage || preBuiltImage == "" {
return nil, fmt.Errorf("MachineOSConfig %s is missing required annotation %s for bootstrap pre-built image support",
mosc.Name, buildconstants.PreBuiltImageAnnotationKey)
}

poolName := mosc.Spec.MachineConfigPool.Name

// Validate the pre-built image format and digest
if err := validatePreBuiltImage(preBuiltImage); err != nil {
return nil, fmt.Errorf("invalid pre-built image %q for MachineOSConfig %s (pool %s): %w",
preBuiltImage, mosc.Name, poolName, err)
}

// Create the component MachineConfig
mc := ctrlcommon.CreatePreBuiltImageMachineConfig(poolName, preBuiltImage, buildconstants.PreBuiltImageAnnotationKey)
preBuiltImageMCs = append(preBuiltImageMCs, mc)
klog.Infof("✓ Validated and created component MachineConfig %s with OSImageURL: %s for pool %s", mc.Name, preBuiltImage, poolName)
}

return preBuiltImageMCs, nil
}

// validatePreBuiltImage validates the pre-built image format using containers/image library
func validatePreBuiltImage(imageSpec string) error {
if imageSpec == "" {
return fmt.Errorf("pre-built image spec cannot be empty")
}

// Use the containers/image library to parse and validate the image reference
ref, err := reference.ParseNamed(imageSpec)
if err != nil {
return fmt.Errorf("pre-built image has invalid format: %w", err)
}

// Ensure the reference has a digest (is canonical)
canonical, ok := ref.(reference.Canonical)
if !ok {
return fmt.Errorf("pre-built image must use digested format (image@sha256:digest), got: %q", imageSpec)
}

// Validate the digest using the go-digest library
if err := canonical.Digest().Validate(); err != nil {
return fmt.Errorf("pre-built image has invalid digest: %w", err)
}

// Ensure it's specifically a SHA256 digest (which is what we expect for container images)
if canonical.Digest().Algorithm() != digest.SHA256 {
return fmt.Errorf("pre-built image must use SHA256 digest, got %s: %q", canonical.Digest().Algorithm(), imageSpec)
}

return nil
}
69 changes: 69 additions & 0 deletions pkg/controller/bootstrap/bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,72 @@ func TestBootstrapRun(t *testing.T) {
})
}
}

func TestValidatePreBuiltImage(t *testing.T) {
tests := []struct {
name string
imageSpec string
expectedError bool
errorContains string
}{
{
name: "Valid image with proper digest format",
imageSpec: "registry.example.com/test@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
expectedError: false,
},
{
name: "Empty image spec should fail",
imageSpec: "",
expectedError: true,
errorContains: "cannot be empty",
},
{
name: "Image without digest should fail",
imageSpec: "registry.example.com/test:latest",
expectedError: true,
errorContains: "must use digested format",
},
{
name: "Image with invalid digest length should fail",
imageSpec: "registry.example.com/test@sha256:12345",
expectedError: true,
errorContains: "invalid reference format",
},
{
name: "Image with invalid digest characters should fail",
imageSpec: "registry.example.com/test@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdez",
expectedError: true,
errorContains: "invalid reference format",
},
{
name: "Image with uppercase digest should fail",
imageSpec: "registry.example.com/test@sha256:1234567890ABCDEF1234567890abcdef1234567890abcdef1234567890abcdef",
expectedError: true,
errorContains: "invalid checksum digest format",
},
{
name: "Image with MD5 digest should fail",
imageSpec: "registry.example.com/test@md5:1234567890abcdef1234567890abcdef",
expectedError: true,
errorContains: "unsupported digest algorithm",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validatePreBuiltImage(tt.imageSpec)

if tt.expectedError && err == nil {
t.Errorf("Expected error but got none")
}
if !tt.expectedError && err != nil {
t.Errorf("Unexpected error: %v", err)
}
if tt.expectedError && err != nil && tt.errorContains != "" {
if !strings.Contains(err.Error(), tt.errorContains) {
t.Errorf("Expected error to contain %q, but got: %v", tt.errorContains, err)
}
}
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
apiVersion: machineconfiguration.openshift.io/v1
kind: MachineOSConfig
metadata:
name: layered-worker
annotations:
machineconfiguration.openshift.io/pre-built-image: "quay.io/example/layered-rhcos:latest@sha256:abcd1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab"
spec:
machineConfigPool:
name: layered-worker
imageBuilder:
imageBuilderType: Job
baseImagePullSecret:
name: pull-secret
renderedImagePushSecret:
name: push-secret
renderedImagePushSpec: quay.io/example/layered-rhcos:latest
containerFile:
- containerfileArch: NoArch
content: |
FROM configs AS final
RUN rpm-ostree install httpd && \
ostree container commit
20 changes: 20 additions & 0 deletions pkg/controller/build/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ const (
TargetMachineConfigPoolLabelKey = "machineconfiguration.openshift.io/target-machine-config-pool"
)

// New labels for pre-built image tracking
const (
// PreBuiltImageLabelKey marks MachineOSBuild objects created from pre-built images
PreBuiltImageLabelKey = "machineconfiguration.openshift.io/pre-built-image"
)

// Annotations added to all ephemeral build objects BuildController creates.
const (
MachineOSBuildNameAnnotationKey = "machineconfiguration.openshift.io/machine-os-build"
Expand All @@ -36,6 +42,20 @@ const (
RebuildMachineOSConfigAnnotationKey string = "machineconfiguration.openshift.io/rebuild"
)

// New annotations for pre-built image support
const (
// PreBuiltImageAnnotationKey indicates a MachineOSConfig should be seeded with a pre-built image
PreBuiltImageAnnotationKey = "machineconfiguration.openshift.io/pre-built-image"
// PreBuiltImageSeededAnnotationKey indicates that the initial synthetic MOSB has been created for this MOSC
PreBuiltImageSeededAnnotationKey = "machineconfiguration.openshift.io/pre-built-image-seeded"
)

// Component MachineConfig naming for pre-built images
const (
// PreBuiltImageMachineConfigPrefix is the prefix for component MCs that set osImageURL from pre-built images
PreBuiltImageMachineConfigPrefix = "10-prebuiltimage-osimageurl-"
)

// Entitled build secret names
const (
// Name of the etc-pki-entitlement secret from the openshift-config-managed namespace.
Expand Down
36 changes: 36 additions & 0 deletions pkg/controller/build/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,42 @@ func hasRebuildAnnotation(mosc *mcfgv1.MachineOSConfig) bool {
return metav1.HasAnnotation(mosc.ObjectMeta, constants.RebuildMachineOSConfigAnnotationKey)
}

// hasPreBuiltImageAnnotation checks if a MachineOSConfig has the pre-built image annotation.
func hasPreBuiltImageAnnotation(mosc *mcfgv1.MachineOSConfig) bool {
_, exists := mosc.Annotations[constants.PreBuiltImageAnnotationKey]
return exists
}

// hasPreBuiltImageSeededAnnotation checks if a MachineOSConfig has been seeded with a pre-built image.
func hasPreBuiltImageSeededAnnotation(mosc *mcfgv1.MachineOSConfig) bool {
_, exists := mosc.Annotations[constants.PreBuiltImageSeededAnnotationKey]
return exists
}

// getPreBuiltImage returns the pre-built image from a MachineOSConfig's annotations.
// Returns the image string and a boolean indicating if it exists and is non-empty.
func getPreBuiltImage(mosc *mcfgv1.MachineOSConfig) (string, bool) {
image, exists := mosc.Annotations[constants.PreBuiltImageAnnotationKey]
return image, exists && image != ""
}

// shouldSeedWithPreBuiltImage determines if a MachineOSConfig should be seeded with a pre-built image.
// Returns true if:
// - The MOSC has a pre-built image annotation
// - The MOSC has NOT been seeded yet
// - The MOSC does NOT have a current build annotation
func shouldSeedWithPreBuiltImage(mosc *mcfgv1.MachineOSConfig) bool {
return hasPreBuiltImageAnnotation(mosc) &&
!hasPreBuiltImageSeededAnnotation(mosc) &&
!hasCurrentBuildAnnotation(mosc)
}

// isPreBuiltImageAwaitingSeeding checks if a MOSC has pre-built image annotation but hasn't been seeded.
// This is useful for skipping normal build workflows when the seeding workflow should handle it.
func isPreBuiltImageAwaitingSeeding(mosc *mcfgv1.MachineOSConfig) bool {
return hasPreBuiltImageAnnotation(mosc) && !hasPreBuiltImageSeededAnnotation(mosc)
}

// Looks at the error chain for the given error and determines if the error
// should be ignored or not based upon whether it is a not found error. If it
// should be ignored, this will log the error as well as the name and kind of
Expand Down
Loading