Skip to content

Commit 9c3bf0f

Browse files
Merge pull request #4025 from Tal-or/4.21_cherry_pick_manual_OCPBUGS-84690
[manual] [release-4.21] OCPBUGS-85647: objectstate/rte: per-pool MachineConfig state with paused MCP awareness
2 parents b0b0878 + d50b242 commit 9c3bf0f

7 files changed

Lines changed: 717 additions & 51 deletions

File tree

internal/controller/numaresourcesoperator_controller.go

Lines changed: 77 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"fmt"
2222
"reflect"
23+
"strings"
2324
"time"
2425

2526
appsv1 "k8s.io/api/apps/v1"
@@ -28,9 +29,11 @@ import (
2829
apiextensionv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2930
apiequality "k8s.io/apimachinery/pkg/api/equality"
3031
apierrors "k8s.io/apimachinery/pkg/api/errors"
32+
apimeta "k8s.io/apimachinery/pkg/api/meta"
3133
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3234
"k8s.io/apimachinery/pkg/labels"
3335
"k8s.io/apimachinery/pkg/runtime"
36+
"k8s.io/apimachinery/pkg/util/sets"
3437
"k8s.io/client-go/tools/record"
3538
"k8s.io/klog/v2"
3639

@@ -139,6 +142,14 @@ func (r *NUMAResourcesOperatorReconciler) Reconcile(ctx context.Context, req ctr
139142
return ctrl.Result{}, err
140143
}
141144

145+
initialStatus := *instance.Status.DeepCopy()
146+
if len(initialStatus.Conditions) == 0 {
147+
instance.Status.Conditions = status.NewNUMAResourcesOperatorConditions()
148+
} else {
149+
// on upgrade, backfill conditions added in newer versions
150+
instance.Status.Conditions = status.EnsureNUMAResourcesOperatorConditions(instance.Status.Conditions)
151+
}
152+
142153
if req.Name != objectnames.DefaultNUMAResourcesOperatorCrName {
143154
err := fmt.Errorf("incorrect NUMAResourcesOperator resource name: %s", instance.Name)
144155
return r.degradeStatus(ctx, instance, status.ConditionTypeIncorrectNUMAResourcesOperatorResourceName, err)
@@ -202,6 +213,11 @@ func updateStatusConditionsIfNeeded(instance *nropv1.NUMAResourcesOperator, cond
202213
ObservedGeneration: instance.Generation,
203214
}, time.Now())
204215
if ok {
216+
for _, c := range instance.Status.Conditions {
217+
if status.FindCondition(conditions, c.Type) == nil {
218+
conditions = append(conditions, c)
219+
}
220+
}
205221
instance.Status.Conditions = conditions
206222
}
207223
}
@@ -247,7 +263,7 @@ func (r *NUMAResourcesOperatorReconciler) reconcileResourceAPI(ctx context.Conte
247263
func (r *NUMAResourcesOperatorReconciler) reconcileResourceMachineConfig(ctx context.Context, instance *nropv1.NUMAResourcesOperator, existing *rtestate.ExistingManifests, trees []nodegroupv1.Tree) intreconcile.Step {
248264
// we need to sync machine configs first and wait for the MachineConfigPool updates
249265
// before checking additional components for updates
250-
mcpUpdatedFunc, err := r.syncMachineConfigs(ctx, instance, existing, trees)
266+
waitByPool, pausedMCPNames, err := r.syncMachineConfigs(ctx, instance, existing, trees)
251267
if err != nil {
252268
r.Recorder.Eventf(instance, corev1.EventTypeWarning, "FailedMCSync", "Failed to set up machine configuration for worker nodes: %v", err)
253269
err = fmt.Errorf("failed to sync machine configs: %w", err)
@@ -257,8 +273,9 @@ func (r *NUMAResourcesOperatorReconciler) reconcileResourceMachineConfig(ctx con
257273

258274
// MCO needs to update the SELinux context removal and other stuff, and need to trigger a reboot.
259275
// It can take a while.
260-
mcpStatuses, mcpNamePending := syncMachineConfigPoolsStatuses(instance.Name, trees, r.ForwardMCPConds, mcpUpdatedFunc)
276+
mcpStatuses, mcpNamePending := syncMachineConfigPoolsStatuses(instance.Name, trees, r.ForwardMCPConds, waitByPool)
261277
instance.Status.MachineConfigPools = mcpStatuses
278+
instance.Status.Conditions = updateMachineConfigPoolPausedCondition(instance.Status.Conditions, instance.Generation, pausedMCPNames)
262279

263280
if mcpNamePending != "" {
264281
// the Machine Config Pool still did not apply the machine config, wait for one minute
@@ -305,7 +322,9 @@ func (r *NUMAResourcesOperatorReconciler) reconcileResource(ctx context.Context,
305322
existing := rtestate.FromClient(ctx, r.Client, r.Platform, r.RTEManifests, instance, trees, r.Namespace)
306323

307324
if r.Platform == platform.OpenShift {
308-
if step := r.reconcileResourceMachineConfig(ctx, instance, existing, trees); step.EarlyStop() {
325+
var step intreconcile.Step
326+
step = r.reconcileResourceMachineConfig(ctx, instance, existing, trees)
327+
if step.EarlyStop() {
309328
updateStatusConditionsIfNeeded(instance, step.ConditionInfo)
310329
return step
311330
}
@@ -408,7 +427,7 @@ func (r *NUMAResourcesOperatorReconciler) syncNodeResourceTopologyAPI(ctx contex
408427
return (updatedCount == len(objStates)), err
409428
}
410429

411-
func (r *NUMAResourcesOperatorReconciler) syncMachineConfigs(ctx context.Context, instance *nropv1.NUMAResourcesOperator, existing *rtestate.ExistingManifests, trees []nodegroupv1.Tree) (rtestate.MCPWaitForUpdatedFunc, error) {
430+
func (r *NUMAResourcesOperatorReconciler) syncMachineConfigs(ctx context.Context, instance *nropv1.NUMAResourcesOperator, existing *rtestate.ExistingManifests, trees []nodegroupv1.Tree) (map[string]rtestate.MCPWaitForUpdatedFunc, sets.Set[string], error) {
412431
klog.V(4).InfoS("Machine Config Sync start", "trees", len(trees))
413432
defer klog.V(4).Info("Machine Config Sync stop")
414433

@@ -418,8 +437,11 @@ func (r *NUMAResourcesOperatorReconciler) syncMachineConfigs(ctx context.Context
418437
// In case of operator upgrade from 4.1X → 4.18, it's necessary to remove the old MachineConfig,
419438
// unless an emergency annotation is provided which forces the operator to use custom policy
420439

421-
objStates, waitFunc := existing.MachineConfigsState(r.RTEManifests)
422-
for _, objState := range objStates {
440+
mcObjStates, pausedMCPNames := existing.MachineConfigsState(r.RTEManifests)
441+
waitByPool := make(map[string]rtestate.MCPWaitForUpdatedFunc, len(mcObjStates))
442+
for _, mcObjState := range mcObjStates {
443+
waitByPool[mcObjState.PoolName] = mcObjState.WaitForUpdated
444+
objState := mcObjState.ObjectState
423445
klog.InfoS("objState", "desired", objState.Desired, "existing", objState.Existing, "createOrUpdate", objState.IsCreateOrUpdate())
424446
if objState.IsCreateOrUpdate() {
425447
if err2 := controllerutil.SetControllerReference(instance, objState.Desired, r.Scheme); err2 != nil {
@@ -438,19 +460,24 @@ func (r *NUMAResourcesOperatorReconciler) syncMachineConfigs(ctx context.Context
438460
break
439461
}
440462
}
441-
return waitFunc, err
463+
return waitByPool, pausedMCPNames, err
442464
}
443465

444-
func syncMachineConfigPoolsStatuses(instanceName string, trees []nodegroupv1.Tree, forwardMCPConds bool, updatedFunc rtestate.MCPWaitForUpdatedFunc) ([]nropv1.MachineConfigPool, string) {
445-
klog.V(4).InfoS("Machine Config Status Sync start", "trees", len(trees))
446-
defer klog.V(4).Info("Machine Config Status Sync stop")
466+
func syncMachineConfigPoolsStatuses(instanceName string, trees []nodegroupv1.Tree, forwardMCPConds bool, waitByPool map[string]rtestate.MCPWaitForUpdatedFunc) ([]nropv1.MachineConfigPool, string) {
467+
klog.V(4).InfoS("Machine Config Pools Status Sync start", "trees", len(trees))
468+
defer klog.V(4).Info("Machine Config Pools Status Sync stop")
447469

448470
mcpStatuses := []nropv1.MachineConfigPool{}
449471
for _, tree := range trees {
450472
for _, mcp := range tree.MachineConfigPools {
451473
mcpStatuses = append(mcpStatuses, extractMCPStatus(mcp, forwardMCPConds))
452474

453-
isUpdated := updatedFunc(instanceName, mcp)
475+
waitFunc, ok := waitByPool[mcp.Name]
476+
if !ok {
477+
continue
478+
}
479+
480+
isUpdated := waitFunc(instanceName, mcp)
454481
klog.V(5).InfoS("Machine Config Pool state", "name", mcp.Name, "instance", instanceName, "updated", isUpdated)
455482

456483
if !isUpdated {
@@ -615,7 +642,8 @@ func (r *NUMAResourcesOperatorReconciler) SetupWithManager(mgr ctrl.Manager) err
615642
return !reflect.DeepEqual(mcpOld.Status.Conditions, mcpNew.Status.Conditions) ||
616643
!apiequality.Semantic.DeepEqual(mcpOld.Labels, mcpNew.Labels) ||
617644
!apiequality.Semantic.DeepEqual(mcpOld.Spec.MachineConfigSelector, mcpNew.Spec.MachineConfigSelector) ||
618-
!apiequality.Semantic.DeepEqual(mcpOld.Spec.NodeSelector, mcpNew.Spec.NodeSelector)
645+
!apiequality.Semantic.DeepEqual(mcpOld.Spec.NodeSelector, mcpNew.Spec.NodeSelector) ||
646+
mcpOld.Spec.Paused != mcpNew.Spec.Paused
619647
},
620648
}
621649

@@ -671,29 +699,35 @@ func (r *NUMAResourcesOperatorReconciler) mcpToNUMAResourceOperator(ctx context.
671699
nro := &nros.Items[i]
672700
mcpLabels := labels.Set(mcp.Labels)
673701
for _, nodeGroup := range nro.Spec.NodeGroups {
674-
if nodeGroup.MachineConfigPoolSelector == nil {
675-
continue
676-
}
677-
678-
nodeGroupSelector, err := metav1.LabelSelectorAsSelector(nodeGroup.MachineConfigPoolSelector)
679-
if err != nil {
680-
klog.Errorf("failed to parse the selector %v", mcp.Spec.NodeSelector)
681-
return nil
682-
}
683-
684-
if nodeGroupSelector.Matches(mcpLabels) {
702+
if nodeGroupMatchesMCP(nodeGroup, mcp.Name, mcpLabels) {
685703
requests = append(requests, reconcile.Request{
686704
NamespacedName: client.ObjectKey{
687705
Name: nro.Name,
688706
},
689707
})
708+
break
690709
}
691710
}
692711
}
693712

694713
return requests
695714
}
696715

716+
func nodeGroupMatchesMCP(nodeGroup nropv1.NodeGroup, mcpName string, mcpLabels labels.Set) bool {
717+
if nodeGroup.PoolName != nil {
718+
return mcpName == *nodeGroup.PoolName
719+
}
720+
if nodeGroup.MachineConfigPoolSelector == nil {
721+
return false
722+
}
723+
selector, err := metav1.LabelSelectorAsSelector(nodeGroup.MachineConfigPoolSelector)
724+
if err != nil {
725+
klog.Errorf("failed to parse the selector %v", nodeGroup.MachineConfigPoolSelector)
726+
return false
727+
}
728+
return selector.Matches(mcpLabels)
729+
}
730+
697731
func (r *NUMAResourcesOperatorReconciler) configMapToNUMAResourceOperator(ctx context.Context, cmObj client.Object) []reconcile.Request {
698732
cm := &corev1.ConfigMap{}
699733

@@ -806,6 +840,26 @@ func isDaemonSetReady(ds *appsv1.DaemonSet) bool {
806840
return ds.Status.DesiredNumberScheduled > 0 && ds.Status.DesiredNumberScheduled == ds.Status.NumberReady
807841
}
808842

843+
func updateMachineConfigPoolPausedCondition(conditions []metav1.Condition, generation int64, pausedMCPNames sets.Set[string]) []metav1.Condition {
844+
pausedStatus := metav1.ConditionFalse
845+
message := ""
846+
if pausedMCPNames.Len() > 0 {
847+
klog.InfoS("detected paused MCPs", "pausedMCPs", pausedMCPNames)
848+
pausedStatus = metav1.ConditionTrue
849+
message = "detected paused MCPs: " + strings.Join(pausedMCPNames.UnsortedList(), ", ")
850+
}
851+
condition := metav1.Condition{
852+
Type: status.ConditionMachineConfigPoolPaused,
853+
Status: pausedStatus,
854+
Reason: status.ConditionMachineConfigPoolPaused,
855+
Message: message,
856+
ObservedGeneration: generation,
857+
LastTransitionTime: metav1.Now(),
858+
}
859+
apimeta.SetStatusCondition(&conditions, condition)
860+
return conditions
861+
}
862+
809863
func getTreesByNodeGroup(ctx context.Context, cli client.Client, nodeGroups []nropv1.NodeGroup, platf platform.Platform) ([]nodegroupv1.Tree, error) {
810864
switch platf {
811865
case platform.OpenShift:

internal/controller/numaresourcesoperator_controller_test.go

Lines changed: 123 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2125,12 +2125,12 @@ var _ = Describe("Test NUMAResourcesOperator Reconcile", func() {
21252125
reconciler = checkSELinuxPolicyProcessing(ctx, nro, mcp1, mcp2)
21262126

21272127
By("upgrading from 4.1X to 4.18")
2128-
Expect(reconciler.Client.Get(context.TODO(), client.ObjectKeyFromObject(nro), nro)).To(Succeed())
2128+
Expect(reconciler.Client.Get(ctx, client.ObjectKeyFromObject(nro), nro)).To(Succeed())
21292129
clearSELinuxPolicyCustomAnnotations(nro)
21302130
Expect(reconciler.Client.Update(context.TODO(), nro)).To(Succeed())
21312131
})
21322132

2133-
It("should delete existing mc", func() {
2133+
It("should delete existing mc", func(ctx context.Context) {
21342134
key := client.ObjectKeyFromObject(nro)
21352135
// removing the annotation will trigger reboot which requires resync after 1 min
21362136
Expect(reconciler.Reconcile(context.TODO(), reconcile.Request{NamespacedName: key})).To(CauseRequeue())
@@ -2139,15 +2139,121 @@ var _ = Describe("Test NUMAResourcesOperator Reconcile", func() {
21392139
Name: objectnames.GetMachineConfigName(nro.Name, mcp1.Name),
21402140
}
21412141
mc := &machineconfigv1.MachineConfig{}
2142-
err := reconciler.Client.Get(context.TODO(), mc1Key, mc)
2142+
err := reconciler.Client.Get(ctx, mc1Key, mc)
21432143
Expect(apierrors.IsNotFound(err)).To(BeTrue(), "MachineConfig %s expected to be deleted; err=%v", mc1Key.Name, err)
21442144

21452145
mc2Key := client.ObjectKey{
21462146
Name: objectnames.GetMachineConfigName(nro.Name, mcp2.Name),
21472147
}
2148-
err = reconciler.Client.Get(context.TODO(), mc2Key, mc)
2148+
err = reconciler.Client.Get(ctx, mc2Key, mc)
21492149
Expect(apierrors.IsNotFound(err)).To(BeTrue(), "MachineConfig %s expected to be deleted; err=%v", mc2Key.Name, err)
21502150
})
2151+
2152+
It("should converge with mixed policy: one pool custom, one pool default", func(ctx context.Context) {
2153+
nro.Spec.NodeGroups[0].Annotations[annotations.SELinuxPolicyConfigAnnotation] = annotations.SELinuxPolicyCustom
2154+
// ng2 has no custom annotation -> default policy (MC deletion)
2155+
2156+
var err error
2157+
reconciler, err = NewFakeNUMAResourcesOperatorReconciler(platform.OpenShift, defaultOCPVersion, nro, mcp1, mcp2)
2158+
Expect(err).ToNot(HaveOccurred())
2159+
2160+
key := client.ObjectKeyFromObject(nro)
2161+
2162+
By("First reconcile: MC created for pool1, MC deleted for pool2, waiting for MCO")
2163+
Expect(reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: key})).To(CauseRequeue())
2164+
2165+
By("Make pool1 ready with MC present (custom policy)")
2166+
Expect(reconciler.Client.Get(ctx, client.ObjectKeyFromObject(mcp1), mcp1)).To(Succeed())
2167+
ensureMCPIsReady(mcp1, nro.Name)
2168+
Expect(reconciler.Client.Update(ctx, mcp1)).To(Succeed())
2169+
2170+
By("Make pool2 ready after MC deletion (default policy)")
2171+
Expect(reconciler.Client.Get(ctx, client.ObjectKeyFromObject(mcp2), mcp2)).To(Succeed())
2172+
ensureMCPIsReadyAfterMCDeletion(mcp2)
2173+
Expect(reconciler.Client.Update(ctx, mcp2)).To(Succeed())
2174+
2175+
By("Second reconcile: should converge")
2176+
Expect(reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: key})).ToNot(CauseRequeue())
2177+
2178+
By("Verify DaemonSets for both pools exist")
2179+
ds := &appsv1.DaemonSet{}
2180+
mcp1DSKey := client.ObjectKey{
2181+
Name: objectnames.GetComponentName(nro.Name, mcp1.Name),
2182+
Namespace: testNamespace,
2183+
}
2184+
Expect(reconciler.Client.Get(ctx, mcp1DSKey, ds)).To(Succeed())
2185+
2186+
mcp2DSKey := client.ObjectKey{
2187+
Name: objectnames.GetComponentName(nro.Name, mcp2.Name),
2188+
Namespace: testNamespace,
2189+
}
2190+
Expect(reconciler.Client.Get(ctx, mcp2DSKey, ds)).To(Succeed())
2191+
2192+
By("Verify NRO status is Available with no paused condition")
2193+
Expect(reconciler.Client.Get(ctx, client.ObjectKeyFromObject(nro), nro)).To(Succeed())
2194+
Expect(nro).To(BeInCondition(status.ConditionAvailable))
2195+
2196+
pausedCond := getConditionByType(nro.Status.Conditions, status.ConditionMachineConfigPoolPaused)
2197+
Expect(pausedCond).ToNot(BeNil())
2198+
Expect(pausedCond.Status).To(Equal(metav1.ConditionFalse))
2199+
})
2200+
It("should converge when one pool is paused", func(ctx context.Context) {
2201+
// neither pool has custom annotation -> both in MC deletion path
2202+
mcp2.Spec.Paused = true
2203+
2204+
var err error
2205+
reconciler, err = NewFakeNUMAResourcesOperatorReconciler(platform.OpenShift, defaultOCPVersion, nro, mcp1, mcp2)
2206+
Expect(err).ToNot(HaveOccurred())
2207+
2208+
key := client.ObjectKeyFromObject(nro)
2209+
2210+
By("Reconcile: pool1 converges (default policy, no MC to wait for), pool2 skipped (paused)")
2211+
Expect(reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: key})).ToNot(CauseRequeue())
2212+
2213+
By("Verify DaemonSets for both pools exist")
2214+
ds := &appsv1.DaemonSet{}
2215+
mcp1DSKey := client.ObjectKey{
2216+
Name: objectnames.GetComponentName(nro.Name, mcp1.Name),
2217+
Namespace: testNamespace,
2218+
}
2219+
Expect(reconciler.Client.Get(ctx, mcp1DSKey, ds)).To(Succeed())
2220+
2221+
mcp2DSKey := client.ObjectKey{
2222+
Name: objectnames.GetComponentName(nro.Name, mcp2.Name),
2223+
Namespace: testNamespace,
2224+
}
2225+
Expect(reconciler.Client.Get(ctx, mcp2DSKey, ds)).To(Succeed())
2226+
2227+
By("Verify NRO status is Available with paused condition")
2228+
Expect(reconciler.Client.Get(ctx, client.ObjectKeyFromObject(nro), nro)).To(Succeed())
2229+
Expect(nro).To(BeInCondition(status.ConditionAvailable))
2230+
2231+
pausedCond := getConditionByType(nro.Status.Conditions, status.ConditionMachineConfigPoolPaused)
2232+
Expect(pausedCond).ToNot(BeNil())
2233+
Expect(pausedCond.Status).To(Equal(metav1.ConditionTrue))
2234+
})
2235+
2236+
It("should report paused condition while Progressing", func(ctx context.Context) {
2237+
// pool1 has custom policy -> MC created, needs MCO rollout
2238+
// pool2 is paused -> skipped
2239+
nro.Spec.NodeGroups[0].Annotations[annotations.SELinuxPolicyConfigAnnotation] = annotations.SELinuxPolicyCustom
2240+
mcp2.Spec.Paused = true
2241+
2242+
var err error
2243+
reconciler, err = NewFakeNUMAResourcesOperatorReconciler(platform.OpenShift, defaultOCPVersion, nro, mcp1, mcp2)
2244+
Expect(err).ToNot(HaveOccurred())
2245+
2246+
key := client.ObjectKeyFromObject(nro)
2247+
2248+
By("First reconcile: MC created for pool1, pool2 skipped (paused), waiting for MCO on pool1")
2249+
Expect(reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: key})).To(CauseRequeue())
2250+
2251+
By("Verify paused condition is set even while Progressing")
2252+
Expect(reconciler.Client.Get(ctx, client.ObjectKeyFromObject(nro), nro)).To(Succeed())
2253+
pausedCond := getConditionByType(nro.Status.Conditions, status.ConditionMachineConfigPoolPaused)
2254+
Expect(pausedCond).ToNot(BeNil(), "paused condition should be set during Progressing phase")
2255+
Expect(pausedCond.Status).To(Equal(metav1.ConditionTrue))
2256+
})
21512257
})
21522258
})
21532259

@@ -2280,8 +2386,10 @@ func checkSELinuxPolicyProcessing(ctx context.Context, nro *nropv1.NUMAResources
22802386
ensureMCPIsReady(mcp1, nro.Name)
22812387
Expect(reconciler.Client.Update(ctx, mcp1)).To(Succeed())
22822388

2389+
// Node group 2 has no custom SELinux policy: wait logic expects our MachineConfig to be *absent* from
2390+
// the pool, so do not put it in Configuration.Source (unlike the pool that has custom policy).
22832391
Expect(reconciler.Client.Get(ctx, client.ObjectKeyFromObject(mcp2), mcp2)).To(Succeed())
2284-
ensureMCPIsReady(mcp2, nro.Name)
2392+
ensureMCPIsReadyAfterMCDeletion(mcp2)
22852393
Expect(reconciler.Client.Update(ctx, mcp2)).To(Succeed())
22862394

22872395
// triggering a second reconcile will create the RTEs and fully update the statuses making the operator in Available condition -> no more reconciliation needed thus the result is clean
@@ -2396,6 +2504,16 @@ func ensureMCPIsReady(mcp *machineconfigv1.MachineConfigPool, nroName string) {
23962504
}
23972505
}
23982506

2507+
func ensureMCPIsReadyAfterMCDeletion(mcp *machineconfigv1.MachineConfigPool) {
2508+
mcp.Status.Configuration.Source = nil
2509+
mcp.Status.Conditions = []machineconfigv1.MachineConfigPoolCondition{
2510+
{
2511+
Type: machineconfigv1.MachineConfigPoolUpdated,
2512+
Status: corev1.ConditionTrue,
2513+
},
2514+
}
2515+
}
2516+
23992517
func BeStringPercentageAtLeast(amount int) gomegatypes.GomegaMatcher {
24002518
return gcustom.MakeMatcher(func(val string) (bool, error) {
24012519
if !strings.HasSuffix(val, "%") {

0 commit comments

Comments
 (0)