Skip to content

Commit 30f8d17

Browse files
Tal-orffromani
andcommitted
tests: add unit and e2e tests for MachineConfigsState
Add unit tests for MachineConfigsState covering custom/default/mixed policies, paused pools, and edge cases. Add e2e test for mixed SELinux policy across node groups. Co-Authored-By: Francesco Romani <fromani@redhat.com> AIA: Primarily AI, New content, Human-initiated, Reviewed, Claude Opus 4.6 v1.0 Signed-off-by: Talor Itzhak <titzhak@redhat.com>
1 parent ee59543 commit 30f8d17

3 files changed

Lines changed: 408 additions & 4 deletions

File tree

internal/controller/numaresourcesoperator_controller_test.go

Lines changed: 4 additions & 4 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,13 +2139,13 @@ 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
})
21512151

Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*
14+
* Copyright 2025 Red Hat, Inc.
15+
*/
16+
17+
package rte
18+
19+
import (
20+
"testing"
21+
22+
corev1 "k8s.io/api/core/v1"
23+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
25+
machineconfigv1 "github.com/openshift/api/machineconfiguration/v1"
26+
27+
rtemanifests "github.com/k8stopologyawareschedwg/deployer/pkg/manifests/rte"
28+
29+
nropv1 "github.com/openshift-kni/numaresources-operator/api/v1"
30+
nodegroupv1 "github.com/openshift-kni/numaresources-operator/api/v1/helper/nodegroup"
31+
"github.com/openshift-kni/numaresources-operator/internal/api/annotations"
32+
"github.com/openshift-kni/numaresources-operator/pkg/objectnames"
33+
)
34+
35+
const testInstanceName = "test-nro"
36+
37+
func newTestMCP(name string) *machineconfigv1.MachineConfigPool {
38+
labels := map[string]string{name: name}
39+
return &machineconfigv1.MachineConfigPool{
40+
ObjectMeta: metav1.ObjectMeta{
41+
Name: name,
42+
Labels: labels,
43+
},
44+
Spec: machineconfigv1.MachineConfigPoolSpec{
45+
MachineConfigSelector: &metav1.LabelSelector{
46+
MatchLabels: labels,
47+
},
48+
NodeSelector: &metav1.LabelSelector{
49+
MatchLabels: labels,
50+
},
51+
},
52+
}
53+
}
54+
55+
func newTestTree(mcp *machineconfigv1.MachineConfigPool, annots map[string]string) nodegroupv1.Tree {
56+
return nodegroupv1.Tree{
57+
NodeGroup: &nropv1.NodeGroup{
58+
Annotations: annots,
59+
},
60+
MachineConfigPools: []*machineconfigv1.MachineConfigPool{mcp},
61+
}
62+
}
63+
64+
func newTestExistingManifests(trees []nodegroupv1.Tree, mcNames ...string) *ExistingManifests {
65+
machineConfigs := make(map[string]machineConfigManifest, len(mcNames))
66+
for _, mcName := range mcNames {
67+
machineConfigs[mcName] = machineConfigManifest{
68+
machineConfig: &machineconfigv1.MachineConfig{
69+
ObjectMeta: metav1.ObjectMeta{
70+
Name: mcName,
71+
},
72+
},
73+
}
74+
}
75+
return &ExistingManifests{
76+
instance: &nropv1.NUMAResourcesOperator{
77+
ObjectMeta: metav1.ObjectMeta{Name: testInstanceName},
78+
},
79+
trees: trees,
80+
machineConfigs: machineConfigs,
81+
namespace: "test-ns",
82+
}
83+
}
84+
85+
func newTestManifests() Manifests {
86+
return Manifests{
87+
Core: rtemanifests.Manifests{
88+
MachineConfig: &machineconfigv1.MachineConfig{
89+
ObjectMeta: metav1.ObjectMeta{
90+
Name: "base-mc",
91+
},
92+
},
93+
},
94+
}
95+
}
96+
97+
func mcpWithSourceAndCondition(name, instanceName string, hasSource bool, updatedStatus corev1.ConditionStatus) *machineconfigv1.MachineConfigPool {
98+
mcp := newTestMCP(name)
99+
if hasSource {
100+
mcp.Status.Configuration.Source = []corev1.ObjectReference{
101+
{Name: objectnames.GetMachineConfigName(instanceName, name)},
102+
}
103+
}
104+
mcp.Status.Conditions = []machineconfigv1.MachineConfigPoolCondition{
105+
{
106+
Type: machineconfigv1.MachineConfigPoolUpdated,
107+
Status: updatedStatus,
108+
},
109+
}
110+
return mcp
111+
}
112+
113+
func TestMachineConfigsState(t *testing.T) {
114+
t.Run("nil core MachineConfig", func(t *testing.T) {
115+
mcp := newTestMCP("pool-a")
116+
tree := newTestTree(mcp, nil)
117+
mcName := objectnames.GetMachineConfigName(testInstanceName, mcp.Name)
118+
em := newTestExistingManifests([]nodegroupv1.Tree{tree}, mcName)
119+
120+
mf := Manifests{}
121+
got, _ := em.MachineConfigsState(mf)
122+
if len(got) != 0 {
123+
t.Fatalf("expected empty result, got %d entries", len(got))
124+
}
125+
})
126+
127+
t.Run("single pool custom policy", func(t *testing.T) {
128+
mcp := newTestMCP("pool-a")
129+
tree := newTestTree(mcp, map[string]string{
130+
annotations.SELinuxPolicyConfigAnnotation: annotations.SELinuxPolicyCustom,
131+
})
132+
mcName := objectnames.GetMachineConfigName(testInstanceName, mcp.Name)
133+
em := newTestExistingManifests([]nodegroupv1.Tree{tree}, mcName)
134+
135+
got, _ := em.MachineConfigsState(newTestManifests())
136+
if len(got) != 1 {
137+
t.Fatalf("expected 1 entry, got %d", len(got))
138+
}
139+
if got[0].PoolName != "pool-a" {
140+
t.Fatalf("expected pool name %q, got %q", "pool-a", got[0].PoolName)
141+
}
142+
if got[0].Paused {
143+
t.Fatal("expected Paused to be false")
144+
}
145+
if got[0].Desired == nil {
146+
t.Fatal("expected non-nil Desired for custom policy pool")
147+
}
148+
149+
mcpReady := mcpWithSourceAndCondition("pool-a", testInstanceName, true, corev1.ConditionTrue)
150+
if !got[0].WaitForUpdated(testInstanceName, mcpReady) {
151+
t.Fatal("expected WaitForUpdated to return true when MC is present")
152+
}
153+
mcpNotReady := mcpWithSourceAndCondition("pool-a", testInstanceName, false, corev1.ConditionTrue)
154+
if got[0].WaitForUpdated(testInstanceName, mcpNotReady) {
155+
t.Fatal("expected WaitForUpdated to return false when MC is absent")
156+
}
157+
})
158+
159+
t.Run("single pool default policy", func(t *testing.T) {
160+
mcp := newTestMCP("pool-a")
161+
tree := newTestTree(mcp, nil)
162+
mcName := objectnames.GetMachineConfigName(testInstanceName, mcp.Name)
163+
em := newTestExistingManifests([]nodegroupv1.Tree{tree}, mcName)
164+
165+
got, _ := em.MachineConfigsState(newTestManifests())
166+
if len(got) != 1 {
167+
t.Fatalf("expected 1 entry, got %d", len(got))
168+
}
169+
if got[0].PoolName != "pool-a" {
170+
t.Fatalf("expected pool name %q, got %q", "pool-a", got[0].PoolName)
171+
}
172+
if got[0].Paused {
173+
t.Fatal("expected Paused to be false")
174+
}
175+
if got[0].Desired != nil {
176+
t.Fatal("expected nil Desired for default policy pool")
177+
}
178+
179+
mcpReady := mcpWithSourceAndCondition("pool-a", testInstanceName, false, corev1.ConditionTrue)
180+
if !got[0].WaitForUpdated(testInstanceName, mcpReady) {
181+
t.Fatal("expected WaitForUpdated to return true when MC is absent")
182+
}
183+
mcpNotReady := mcpWithSourceAndCondition("pool-a", testInstanceName, true, corev1.ConditionTrue)
184+
if got[0].WaitForUpdated(testInstanceName, mcpNotReady) {
185+
t.Fatal("expected WaitForUpdated to return false when MC is still present")
186+
}
187+
})
188+
189+
t.Run("mixed pools", func(t *testing.T) {
190+
mcpCustom := newTestMCP("pool-custom")
191+
treeCustom := newTestTree(mcpCustom, map[string]string{
192+
annotations.SELinuxPolicyConfigAnnotation: annotations.SELinuxPolicyCustom,
193+
})
194+
195+
mcpDefault := newTestMCP("pool-default")
196+
treeDefault := newTestTree(mcpDefault, nil)
197+
198+
mcNameCustom := objectnames.GetMachineConfigName(testInstanceName, mcpCustom.Name)
199+
mcNameDefault := objectnames.GetMachineConfigName(testInstanceName, mcpDefault.Name)
200+
em := newTestExistingManifests(
201+
[]nodegroupv1.Tree{treeCustom, treeDefault},
202+
mcNameCustom, mcNameDefault,
203+
)
204+
205+
got, _ := em.MachineConfigsState(newTestManifests())
206+
if len(got) != 2 {
207+
t.Fatalf("expected 2 entries, got %d", len(got))
208+
}
209+
210+
var customEntry, defaultEntry MachineConfigObjectState
211+
for _, entry := range got {
212+
switch entry.PoolName {
213+
case "pool-custom":
214+
customEntry = entry
215+
case "pool-default":
216+
defaultEntry = entry
217+
default:
218+
t.Fatalf("unexpected pool name %q", entry.PoolName)
219+
}
220+
}
221+
222+
if customEntry.Desired == nil {
223+
t.Fatal("expected non-nil Desired for custom policy pool")
224+
}
225+
if defaultEntry.Desired != nil {
226+
t.Fatal("expected nil Desired for default policy pool")
227+
}
228+
229+
mcpPresent := mcpWithSourceAndCondition("pool-custom", testInstanceName, true, corev1.ConditionTrue)
230+
if !customEntry.WaitForUpdated(testInstanceName, mcpPresent) {
231+
t.Fatal("custom pool: expected true when MC is present")
232+
}
233+
mcpAbsent := mcpWithSourceAndCondition("pool-custom", testInstanceName, false, corev1.ConditionTrue)
234+
if customEntry.WaitForUpdated(testInstanceName, mcpAbsent) {
235+
t.Fatal("custom pool: expected false when MC is absent")
236+
}
237+
238+
mcpDeleted := mcpWithSourceAndCondition("pool-default", testInstanceName, false, corev1.ConditionTrue)
239+
if !defaultEntry.WaitForUpdated(testInstanceName, mcpDeleted) {
240+
t.Fatal("default pool: expected true when MC is absent")
241+
}
242+
mcpStillPresent := mcpWithSourceAndCondition("pool-default", testInstanceName, true, corev1.ConditionTrue)
243+
if defaultEntry.WaitForUpdated(testInstanceName, mcpStillPresent) {
244+
t.Fatal("default pool: expected false when MC is still present")
245+
}
246+
})
247+
248+
t.Run("pool without MachineConfigSelector", func(t *testing.T) {
249+
mcp := newTestMCP("pool-no-selector")
250+
mcp.Spec.MachineConfigSelector = nil
251+
252+
tree := newTestTree(mcp, nil)
253+
mcName := objectnames.GetMachineConfigName(testInstanceName, mcp.Name)
254+
em := newTestExistingManifests([]nodegroupv1.Tree{tree}, mcName)
255+
256+
got, _ := em.MachineConfigsState(newTestManifests())
257+
if len(got) != 0 {
258+
t.Fatalf("expected empty result for pool without selector, got %d entries", len(got))
259+
}
260+
})
261+
262+
t.Run("pool not in machineConfigs cache", func(t *testing.T) {
263+
mcp := newTestMCP("pool-uncached")
264+
tree := newTestTree(mcp, nil)
265+
em := newTestExistingManifests([]nodegroupv1.Tree{tree})
266+
267+
got, _ := em.MachineConfigsState(newTestManifests())
268+
if len(got) != 0 {
269+
t.Fatalf("expected empty result for uncached pool, got %d entries", len(got))
270+
}
271+
})
272+
273+
t.Run("paused pool", func(t *testing.T) {
274+
mcp := newTestMCP("pool-paused")
275+
mcp.Spec.Paused = true
276+
277+
tree := newTestTree(mcp, nil)
278+
mcName := objectnames.GetMachineConfigName(testInstanceName, mcp.Name)
279+
em := newTestExistingManifests([]nodegroupv1.Tree{tree}, mcName)
280+
281+
got, pausedMCPNames := em.MachineConfigsState(newTestManifests())
282+
if len(got) != 0 {
283+
t.Fatalf("expected 0 entries for paused pool, got %d entries", len(got))
284+
}
285+
if !pausedMCPNames.Has("pool-paused") {
286+
t.Fatal("expected pool-paused to be in pausedMCPNames set")
287+
}
288+
})
289+
290+
t.Run("mixed pools with paused", func(t *testing.T) {
291+
mcpCustom := newTestMCP("pool-custom")
292+
treeCustom := newTestTree(mcpCustom, map[string]string{
293+
annotations.SELinuxPolicyConfigAnnotation: annotations.SELinuxPolicyCustom,
294+
})
295+
296+
mcpDefault := newTestMCP("pool-default")
297+
treeDefault := newTestTree(mcpDefault, nil)
298+
299+
mcpPaused := newTestMCP("pool-paused")
300+
mcpPaused.Spec.Paused = true
301+
treePaused := newTestTree(mcpPaused, nil)
302+
303+
mcNameCustom := objectnames.GetMachineConfigName(testInstanceName, mcpCustom.Name)
304+
mcNameDefault := objectnames.GetMachineConfigName(testInstanceName, mcpDefault.Name)
305+
mcNamePaused := objectnames.GetMachineConfigName(testInstanceName, mcpPaused.Name)
306+
em := newTestExistingManifests(
307+
[]nodegroupv1.Tree{treeCustom, treeDefault, treePaused},
308+
mcNameCustom, mcNameDefault, mcNamePaused,
309+
)
310+
311+
got, pausedMCPNames := em.MachineConfigsState(newTestManifests())
312+
if len(got) != 2 {
313+
t.Fatalf("expected 2 active entries, got %d", len(got))
314+
}
315+
if !pausedMCPNames.Has("pool-paused") {
316+
t.Fatal("expected pool-paused to be in pausedMCPNames set")
317+
}
318+
if pausedMCPNames.Len() != 1 {
319+
t.Fatalf("expected 1 paused pool, got %d", pausedMCPNames.Len())
320+
}
321+
})
322+
}

0 commit comments

Comments
 (0)