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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require (
github.com/golangci/golangci-lint/v2 v2.11.1
github.com/onsi/ginkgo/v2 v2.28.1
github.com/onsi/gomega v1.39.1
github.com/openshift/api v0.0.0-20260317165824-54a3998d81eb
github.com/openshift/api v0.0.0-20260330162214-96f1f5ac7ff2
github.com/openshift/client-go v0.0.0-20260317180604-743f664b82d1
github.com/openshift/cluster-api-actuator-pkg/testutils v0.0.0-20260310144400-bec013a007a8
github.com/openshift/controller-runtime-common v0.0.0-20260318085703-1812aed6dbd2
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -466,8 +466,8 @@ github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
github.com/openshift-cloud-team/cloud-provider-vsphere v1.19.1-0.20260317135518-758abc9d59a5 h1:Mayj50dtdLPzUVmJNHJpM4GpFWq7fcy9FDIoYUfngQ4=
github.com/openshift-cloud-team/cloud-provider-vsphere v1.19.1-0.20260317135518-758abc9d59a5/go.mod h1:3uaiy47HteyMlDjJankjteem/s1hnbRBU1FgbekLMKU=
github.com/openshift/api v0.0.0-20260317165824-54a3998d81eb h1:iwBR3mzmyE3EMFx7R3CQ9lOccTS0dNht8TW82aGITg0=
github.com/openshift/api v0.0.0-20260317165824-54a3998d81eb/go.mod h1:pyVjK0nZ4sRs4fuQVQ4rubsJdahI1PB94LnQ8sGdvxo=
github.com/openshift/api v0.0.0-20260330162214-96f1f5ac7ff2 h1:q89bR1UvKEH9kNh9me1oqLYszKuhaeghorpkO3+DNwY=
github.com/openshift/api v0.0.0-20260330162214-96f1f5ac7ff2/go.mod h1:pyVjK0nZ4sRs4fuQVQ4rubsJdahI1PB94LnQ8sGdvxo=
github.com/openshift/client-go v0.0.0-20260317180604-743f664b82d1 h1:Hr/R38eg5ZJXfbiaHumjJIN1buDZwhsm4ys4npVCXH0=
github.com/openshift/client-go v0.0.0-20260317180604-743f664b82d1/go.mod h1:Za51LlH76ALiQ/aKGBYJXmyJNkA//IDJ+I///30CA2M=
github.com/openshift/cluster-api-actuator-pkg/testutils v0.0.0-20260310144400-bec013a007a8 h1:x62h16RetnB1ZP+zjSM9fsoMz98g95zte+DXeUDF34o=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,9 @@ rules:
- get
- list
- watch
- create
- update
- patch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
Expand Down
207 changes: 152 additions & 55 deletions pkg/controllers/cloud_config_sync_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/predicate"

configv1 "github.com/openshift/api/config/v1"
"github.com/openshift/api/features"
"github.com/openshift/library-go/pkg/operator/configobserver/featuregates"

"github.com/openshift/cluster-cloud-controller-manager-operator/pkg/cloud"
Expand All @@ -31,6 +32,36 @@ const (
cloudConfigControllerDegradedCondition = "CloudConfigControllerDegraded"
)

// isFeatureGateEnabled checks if a feature gate is enabled.
// Returns false if the feature gate is nil.
// This provides a nil-safe way to check feature gates.
func isFeatureGateEnabled(featureGates featuregates.FeatureGate, featureName configv1.FeatureGateName) bool {
if featureGates == nil {
return false
}
return featureGates.Enabled(featureName)
}

// shouldManageManagedConfigMap returns true if CCCMO should manage the
// openshift-config-managed/kube-cloud-config ConfigMap for the given platform.
// This indicates ownership has been migrated from CCO to CCCMO.
//
// For vSphere, this requires the VSphereMultiVCenterDay2 feature gate to be enabled.
func shouldManageManagedConfigMap(platformType configv1.PlatformType, featureGates featuregates.FeatureGate) bool {
switch platformType {
case configv1.VSpherePlatformType:
// Only manage the configmap if the feature gate is enabled
return isFeatureGateEnabled(featureGates, features.FeatureGateVSphereMultiVCenterDay2)
// Future: Add other platforms as they migrate from CCO
// case configv1.AWSPlatformType:
// return true
// case configv1.AzurePlatformType:
// return true
default:
return false
}
}

type CloudConfigReconciler struct {
ClusterOperatorStatusClient
Scheme *runtime.Scheme
Expand Down Expand Up @@ -72,6 +103,24 @@ func (r *CloudConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request)
return ctrl.Result{}, nil
}

// Check if FeatureGateAccess is configured (needed early for transformer)
if r.FeatureGateAccess == nil {
klog.Errorf("FeatureGateAccess is not configured")
if err := r.setDegradedCondition(ctx); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to set conditions for cloud config controller: %v", err)
}
return ctrl.Result{}, fmt.Errorf("FeatureGateAccess is not configured")
}

features, err := r.FeatureGateAccess.CurrentFeatureGates()
if err != nil {
klog.Errorf("unable to get feature gates: %v", err)
if errD := r.setDegradedCondition(ctx); errD != nil {
return ctrl.Result{}, fmt.Errorf("failed to set conditions for cloud config controller: %v", errD)
}
return ctrl.Result{}, err
}

cloudConfigTransformerFn, needsManagedConfigLookup, err := cloud.GetCloudConfigTransformer(infra.Status.PlatformStatus)
if err != nil {
klog.Errorf("unable to get cloud config transformer function; unsupported platform")
Expand All @@ -81,6 +130,7 @@ func (r *CloudConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request)
return ctrl.Result{}, err
}

platformType := infra.Status.PlatformStatus.Type
sourceCM := &corev1.ConfigMap{}
managedConfigFound := false

Expand Down Expand Up @@ -110,20 +160,28 @@ func (r *CloudConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request)
}
}

// Only look for an unmanaged config if the managed one isn't found and a name was specified.
// Fallback: Look for config in openshift-config namespace if not found in managed namespace
// For platforms we manage (e.g., vSphere), we'll use this as the source to populate openshift-config-managed
if !managedConfigFound && infra.Spec.CloudConfig.Name != "" {
openshiftUnmanagedCMKey := client.ObjectKey{
Name: infra.Spec.CloudConfig.Name,
Namespace: OpenshiftConfigNamespace,
}
if err := r.Get(ctx, openshiftUnmanagedCMKey, sourceCM); errors.IsNotFound(err) {
klog.Warningf("managed cloud-config is not found, falling back to default cloud config.")
klog.Warningf("cloud-config not found in either openshift-config-managed or openshift-config namespace")
// For platforms we manage, create an empty source that will be populated by the transformer
if shouldManageManagedConfigMap(platformType, features) {
klog.Infof("Initializing empty config for platform %s", platformType)
sourceCM.Data = map[string]string{defaultConfigKey: ""}
}
} else if err != nil {
klog.Errorf("unable to get cloud-config for sync: %v", err)
if err := r.setDegradedCondition(ctx); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to set conditions for cloud config controller: %v", err)
}
return ctrl.Result{}, err
} else {
klog.V(3).Infof("Found config in openshift-config namespace for platform %s", platformType)
}
}

Expand All @@ -135,24 +193,7 @@ func (r *CloudConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request)
return ctrl.Result{}, err
}

// Check if FeatureGateAccess is configured
if r.FeatureGateAccess == nil {
klog.Errorf("FeatureGateAccess is not configured")
if err := r.setDegradedCondition(ctx); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to set conditions for cloud config controller: %v", err)
}
return ctrl.Result{}, fmt.Errorf("FeatureGateAccess is not configured")
}

features, err := r.FeatureGateAccess.CurrentFeatureGates()
if err != nil {
klog.Errorf("unable to get feature gates: %v", err)
if errD := r.setDegradedCondition(ctx); errD != nil {
return ctrl.Result{}, fmt.Errorf("failed to set conditions for cloud config controller: %v", errD)
}
return ctrl.Result{}, err
}

// Apply transformer if needed
if cloudConfigTransformerFn != nil {
// We ignore stuff in sourceCM.BinaryData. This isn't allowed to
// contain any key that overlaps with those found in sourceCM.Data and
Expand All @@ -167,31 +208,20 @@ func (r *CloudConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request)
sourceCM.Data[defaultConfigKey] = output
}

targetCM := &corev1.ConfigMap{}
targetConfigMapKey := client.ObjectKey{
Namespace: r.ManagedNamespace,
Name: syncedCloudConfigMapName,
}

// If the config does not exist, it will be created later, so we can ignore a Not Found error
if err := r.Get(ctx, targetConfigMapKey, targetCM); err != nil && !errors.IsNotFound(err) {
klog.Errorf("unable to get target cloud-config for sync")
if err := r.setDegradedCondition(ctx); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to set conditions for cloud config controller: %v", err)
}
return ctrl.Result{}, err
}

// Note that the source config map is actually a *transformed* source config map
if r.isCloudConfigEqual(sourceCM, targetCM) {
klog.V(1).Infof("source and target cloud-config content are equal, no sync needed")
if err := r.setAvailableCondition(ctx); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to set conditions for cloud config controller: %v", err)
// For platforms managed by CCCMO, update openshift-config-managed/kube-cloud-config
// with the transformed config so other operators can read from a consistent location
if shouldManageManagedConfigMap(platformType, features) {
if err := r.syncManagedCloudConfig(ctx, sourceCM); err != nil {
klog.Errorf("failed to sync managed cloud config: %v", err)
if err := r.setDegradedCondition(ctx); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to set conditions for cloud config controller: %v", err)
}
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}

if err := r.syncCloudConfigData(ctx, sourceCM, targetCM); err != nil {
// Sync the transformed config to the target configmap for CCM consumption
if err := r.syncCloudConfigData(ctx, sourceCM); err != nil {
klog.Errorf("unable to sync cloud config")
if err := r.setDegradedCondition(ctx); err != nil {
return ctrl.Result{}, fmt.Errorf("failed to set conditions for cloud config controller: %v", err)
Expand Down Expand Up @@ -264,28 +294,95 @@ func (r *CloudConfigReconciler) prepareSourceConfigMap(source *corev1.ConfigMap,
return cloudConfCm, nil
}

// isCloudConfigEqual compares two ConfigMaps to determine if their content is equal.
// It performs a deep comparison of the Data, BinaryData, and Immutable fields.
//
// This function is used to avoid unnecessary updates when the cloud configuration
// content hasn't changed. Metadata fields (labels, annotations, resourceVersion, etc.)
// are intentionally ignored as they don't affect the actual configuration data.
//
// Returns true if both ConfigMaps have identical Data, BinaryData, and Immutable values.
func (r *CloudConfigReconciler) isCloudConfigEqual(source *corev1.ConfigMap, target *corev1.ConfigMap) bool {
return source.Immutable == target.Immutable &&
reflect.DeepEqual(source.Data, target.Data) && reflect.DeepEqual(source.BinaryData, target.BinaryData)
}

func (r *CloudConfigReconciler) syncCloudConfigData(ctx context.Context, source *corev1.ConfigMap, target *corev1.ConfigMap) error {
target.SetName(syncedCloudConfigMapName)
target.SetNamespace(r.ManagedNamespace)
target.Data = source.Data
target.BinaryData = source.BinaryData
target.Immutable = source.Immutable
// syncConfigMapToTarget is a generic helper that syncs a source ConfigMap to a target namespace/name.
// It handles create-or-update logic with optional equality checking to avoid unnecessary updates.
func (r *CloudConfigReconciler) syncConfigMapToTarget(ctx context.Context, source *corev1.ConfigMap, targetName, targetNamespace string, checkEquality bool) error {
if source == nil || source.Data == nil {
return fmt.Errorf("source configmap is nil or has no data")
}

// check if target config exists, create if not
err := r.Get(ctx, client.ObjectKeyFromObject(target), &corev1.ConfigMap{})
targetCM := &corev1.ConfigMap{}
targetKey := client.ObjectKey{
Name: targetName,
Namespace: targetNamespace,
}

if err != nil && errors.IsNotFound(err) {
return r.Create(ctx, target)
} else if err != nil {
return err
// Check if target exists
err := r.Get(ctx, targetKey, targetCM)
if err != nil && !errors.IsNotFound(err) {
return fmt.Errorf("failed to get target configmap %s/%s: %w", targetNamespace, targetName, err)
}

targetExists := err == nil

// Check if update is needed (if requested)
if targetExists && checkEquality {
if r.isCloudConfigEqual(source, targetCM) {
klog.V(3).Infof("Target configmap %s/%s is already up to date", targetNamespace, targetName)
return nil
}
}

// Prepare the target configmap
targetCM.SetName(targetName)
targetCM.SetNamespace(targetNamespace)
targetCM.Data = source.Data
targetCM.BinaryData = source.BinaryData
targetCM.Immutable = source.Immutable

// Create or update
if !targetExists {
klog.Infof("Creating configmap %s/%s", targetNamespace, targetName)
if err := r.Create(ctx, targetCM); err != nil {
return fmt.Errorf("failed to create configmap %s/%s: %w", targetNamespace, targetName, err)
}
return nil
}

klog.V(3).Infof("Updating configmap %s/%s", targetNamespace, targetName)
if err := r.Update(ctx, targetCM); err != nil {
return fmt.Errorf("failed to update configmap %s/%s: %w", targetNamespace, targetName, err)
}
return nil
}

func (r *CloudConfigReconciler) syncCloudConfigData(ctx context.Context, source *corev1.ConfigMap) error {
// Use the generic helper, no equality check (always update for target CM)
return r.syncConfigMapToTarget(ctx, source, syncedCloudConfigMapName, r.ManagedNamespace, false)
}

// syncManagedCloudConfig updates openshift-config-managed/kube-cloud-config with the
// transformed cloud config. This makes the transformed config available to other operators
// while maintaining CCCMO as the owner of this ConfigMap (migrated from CCO).
//
// This function handles the migration of ownership from CCO to CCCMO by:
// - Creating the ConfigMap if it doesn't exist (initial migration)
// - Updating it with transformed config from user source (openshift-config)
// - Making it the single source of truth for other operators
func (r *CloudConfigReconciler) syncManagedCloudConfig(ctx context.Context, source *corev1.ConfigMap) error {
// Validate source has required key
if source != nil && source.Data != nil {
if _, ok := source.Data[defaultConfigKey]; !ok {
return fmt.Errorf("source configmap missing required key: %s", defaultConfigKey)
}
}

return r.Update(ctx, target)
// Use the generic helper with equality check (avoid unnecessary updates)
// For managed config, we want to check equality to reduce churn
return r.syncConfigMapToTarget(ctx, source, managedCloudConfigMapName, OpenshiftManagedConfigNamespace, true)
}

// SetupWithManager sets up the controller with the Manager.
Expand Down
Loading