Skip to content

Re-ordering ManagedClusterAddOn configs has no effect #1600

Description

@KevinFCormier

Describe the bug

Re-ordering entries in ManagedClusterAddOn.spec.configs does not update
the order in status.configReferences. The order is determined by the
first reconcile and never changes, making it impossible to alter config
precedence after initial creation.

Config precedence depends on index ordering — the documented contract on
GetAddOnDeploymentConfigValues (in addon-framework) states that the
highest-index entry takes precedence. However, changing which config
appears last in spec.configs has no effect because
status.configReferences retains its original order.

To Reproduce

Prerequisites: a hub cluster with OCM addon-manager running. This example
uses cluster1 as the managed cluster name, consistent with the
OCM contributing guidelines
for kind-based test environments.

  1. Create an AddOnTemplate with a variable placeholder:
kubectl apply -f - <<'EOF'
apiVersion: addon.open-cluster-management.io/v1alpha1
kind: AddOnTemplate
metadata:
  name: ordering-demo
spec:
  addonName: ordering-demo
  agentSpec:
    workload:
      manifests:
        - apiVersion: v1
          kind: ConfigMap
          metadata:
            name: ordering-demo
            namespace: open-cluster-management-agent-addon
          data:
            greeting: "{{GREETING}}"
EOF
  1. Create two AddOnDeploymentConfig resources with different values:
kubectl apply -f - <<'EOF'
apiVersion: addon.open-cluster-management.io/v1alpha1
kind: AddOnDeploymentConfig
metadata:
  name: greeting-hello
  namespace: cluster1
spec:
  customizedVariables:
    - name: GREETING
      value: "hello"
---
apiVersion: addon.open-cluster-management.io/v1alpha1
kind: AddOnDeploymentConfig
metadata:
  name: greeting-goodbye
  namespace: cluster1
spec:
  customizedVariables:
    - name: GREETING
      value: "goodbye"
EOF
  1. Create a ClusterManagementAddOn referencing the template (no global
    ADC default, so per-cluster configs are the only source):
kubectl apply -f - <<'EOF'
apiVersion: addon.open-cluster-management.io/v1alpha1
kind: ClusterManagementAddOn
metadata:
  name: ordering-demo
  annotations:
    addon.open-cluster-management.io/lifecycle: "addon-manager"
spec:
  addOnMeta:
    displayName: Config Ordering Demo
  installStrategy:
    type: Manual
  supportedConfigs:
    - group: addon.open-cluster-management.io
      resource: addontemplates
      defaultConfig:
        name: ordering-demo
    - group: addon.open-cluster-management.io
      resource: addondeploymentconfigs
EOF
  1. Create the ManagedClusterAddOn with two ADC references — hello
    first, goodbye second:
kubectl apply -f - <<'EOF'
apiVersion: addon.open-cluster-management.io/v1alpha1
kind: ManagedClusterAddOn
metadata:
  name: ordering-demo
  namespace: cluster1
spec:
  configs:
    - group: addon.open-cluster-management.io
      resource: addondeploymentconfigs
      name: greeting-hello
      namespace: cluster1
    - group: addon.open-cluster-management.io
      resource: addondeploymentconfigs
      name: greeting-goodbye
      namespace: cluster1
EOF
  1. Wait for reconciliation, then inspect status.configReferences:
kubectl get managedclusteraddons ordering-demo -n cluster1 \
  -o jsonpath='{range .status.configReferences[*]}{.resource}: {.desiredConfig.name}{"\n"}{end}'

Note the order — greeting-hello appears before greeting-goodbye.

  1. Re-order the configs to put goodbye first:
kubectl apply -f - <<'EOF'
apiVersion: addon.open-cluster-management.io/v1alpha1
kind: ManagedClusterAddOn
metadata:
  name: ordering-demo
  namespace: cluster1
spec:
  configs:
    - group: addon.open-cluster-management.io
      resource: addondeploymentconfigs
      name: greeting-goodbye
      namespace: cluster1
    - group: addon.open-cluster-management.io
      resource: addondeploymentconfigs
      name: greeting-hello
      namespace: cluster1
EOF
  1. Wait for reconciliation and inspect again:
kubectl get managedclusteraddons ordering-demo -n cluster1 \
  -o jsonpath='{range .status.configReferences[*]}{.resource}: {.desiredConfig.name}{"\n"}{end}'

The order in status.configReferences is unchanged — greeting-hello
still appears before greeting-goodbye, despite the spec now listing
goodbye first.

Cleanup:

kubectl delete clustermanagementaddons ordering-demo
kubectl delete addontemplates ordering-demo
kubectl delete addondeploymentconfigs greeting-hello greeting-goodbye -n cluster1

Expected behavior

When the user re-orders entries in ManagedClusterAddOn.spec.configs,
the order in status.configReferences should be updated to reflect the
new ordering, so that precedence changes take effect.

Environment ie: OCM version, Kubernetes version and provider:

  • OCM addon-manager (all versions with multi-GK config support)
  • Kubernetes 4.x / OpenShift 4.x

Additional context

The root cause is in mergeAddonConfig in
pkg/addon/controllers/addonconfiguration/addon_configuration_reconciler.go:

mergedConfigs := make(addonConfigMap)
// First go through the configReferences listed in mca status,
// if the existing config (gvk + namespace + name) is also in the desiredConfigMap,
// append it to mergedConfigs,
// this will save the LastAppliedConfig and LastObservedGeneration from mca status.
for _, configRef := range mcaCopy.Status.ConfigReferences {
    gr := configRef.ConfigGroupResource
    if _, ok := mergedConfigs[gr]; !ok {
        mergedConfigs[gr] = []addonv1beta1.ConfigReference{}
    }
    if _, ok := desiredConfigMap.containsConfig(gr, configRef.DesiredConfig.ConfigReferent); ok {
        mergedConfigs[gr] = append(mergedConfigs[gr], configRef)
    }
}
// Then go through the desiredConfigMap ...

Because the old status.configReferences are iterated first and their
order is preserved, any re-ordering in the desired config map is ignored
for entries that already exist. New entries are appended at the end, but
existing entries keep their original positions.

The fix is to use the desiredConfigMap as the source of truth for
ordering, while preserving LastAppliedConfig and
LastObservedGeneration via a lookup map keyed by
group+resource+name+namespace:

// Build a lookup from old status to preserve rollout tracking fields
oldStatusByKey := map[configKey]addonv1beta1.ConfigReference{}
for _, configRef := range mcaCopy.Status.ConfigReferences {
    key := configKey{...}
    oldStatusByKey[key] = configRef
}

// Iterate desiredConfigMap in sorted GR order — source of truth for ordering
configRefs := []addonv1beta1.ConfigReference{}
for _, gr := range desiredConfigMap.orderedKeys() {
    for _, configRef := range desiredConfigMap[gr] {
        key := configKey{...}
        if old, ok := oldStatusByKey[key]; ok {
            old.DesiredConfig = configRef.DesiredConfig.DeepCopy()
            configRefs = append(configRefs, old)
        } else {
            configRefs = append(configRefs, configRef)
        }
    }
}

Related issue: There is a separate bug in addon-framework where
GetAddOnConfigRef returns the first match instead of the last, which
compounds this issue. See open-cluster-management-io/addon-framework#387

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions