1 package integration
2
3 import (
4 "fmt"
5 "testing"
6 "time"
7
8 "github.com/stretchr/testify/require"
9 "gotest.tools/v3/assert/cmp"
10 corev1 "k8s.io/api/core/v1"
11 kresource "k8s.io/apimachinery/pkg/api/resource"
12 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13 "sigs.k8s.io/controller-runtime/pkg/client"
14
15 "edge-infra.dev/pkg/k8s/runtime/conditions"
16 "edge-infra.dev/pkg/k8s/testing/kmp"
17 v2 "edge-infra.dev/pkg/sds/display/k8s/apis/v2"
18 xserverconfig "edge-infra.dev/pkg/sds/display/k8s/controllers/xserver/config"
19 "edge-infra.dev/pkg/sds/ien/resource"
20 "edge-infra.dev/test/f2"
21 "edge-infra.dev/test/f2/x/ktest"
22 )
23
24 var displayctlHostname = "displayctl-host"
25
26 var (
27 card0HDMI1 = v2.DisplayPort("card0-HDMI1")
28 card0HDMI2 = v2.DisplayPort("card0-HDMI2")
29 card0DP1 = v2.DisplayPort("card0-DP1")
30 card0DP2 = v2.DisplayPort("card0-DP2")
31
32 elo4098 = v2.MPID("ELO-4098")
33 ncr22888 = v2.MPID("NCR-22888")
34 vsc9365 = v2.MPID("VSC-9365")
35
36 eloTouchSolutionsUSB = v2.InputDeviceName("Elo Touch Solutions Elo Touch Solutions Pcap USB Interface")
37 eGalaxEXC3189 = v2.InputDeviceName("eGalax Inc. eGalaxTouch EXC3189-2506-09.00.00.00")
38 eGalaxEXC3189Mouse = v2.InputDeviceName("eGalax Inc. eGalaxTouch EXC3189-2506-09.00.00.00 Mouse")
39 iSolutionMultiTouch = v2.InputDeviceName("iSolution multitouch")
40
41 primary = v2.Primary(true)
42 notPrimary = v2.Primary(false)
43
44 dpmsEnabled = true
45 dpmsDisabled = false
46
47 zeroSeconds = 0
48 sixtySeconds = 60
49 )
50
51 var errCouldNotCastObjectToNodeDisplayConfig = fmt.Errorf("could not cast object to *v2.NodeDisplayConfig")
52
53 var expectedDefaultDisplayConfig = &v2.DisplayConfig{
54 Displays: []v2.Display{
55 {
56 DisplayPort: card0DP1,
57 MPID: &vsc9365,
58 Primary: ¬Primary,
59 Orientation: &v2.NormalOrientation,
60 Resolution: &v2.Resolution{
61 Width: 1280,
62 Height: 1084,
63 },
64 },
65 {
66 DisplayPort: card0HDMI1,
67 MPID: &elo4098,
68 Primary: &primary,
69 Orientation: &v2.NormalOrientation,
70 Resolution: &v2.Resolution{
71 Width: 1260,
72 Height: 720,
73 },
74 InputDeviceMappings: []v2.InputDeviceName{
75 eloTouchSolutionsUSB,
76 },
77 },
78 {
79 DisplayPort: card0HDMI2,
80 MPID: &ncr22888,
81 Primary: ¬Primary,
82 Orientation: &v2.NormalOrientation,
83 Resolution: &v2.Resolution{
84 Width: 1920,
85 Height: 1080,
86 },
87 InputDeviceMappings: []v2.InputDeviceName{
88 eGalaxEXC3189,
89 eGalaxEXC3189Mouse,
90 },
91 },
92 },
93
94 DPMS: &v2.DPMS{
95 Enabled: &dpmsDisabled,
96 BlankTime: &zeroSeconds,
97 StandbyTime: &zeroSeconds,
98 SuspendTime: &zeroSeconds,
99 OffTime: &zeroSeconds,
100 },
101 Layout: v2.Layout{card0HDMI1, card0HDMI2, card0DP1},
102 }
103
104 var expectedCustomDisplayConfig = &v2.DisplayConfig{
105 Displays: []v2.Display{
106 {
107 DisplayPort: card0DP1,
108 MPID: &vsc9365,
109 Primary: &primary,
110 Orientation: &v2.NormalOrientation,
111 Resolution: &v2.Resolution{
112 Width: 1280,
113 Height: 1084,
114 },
115 InputDeviceMappings: []v2.InputDeviceName{
116 iSolutionMultiTouch,
117 },
118 },
119 {
120 DisplayPort: card0HDMI1,
121 MPID: &elo4098,
122 Primary: ¬Primary,
123 Orientation: &v2.NormalOrientation,
124 Resolution: &v2.Resolution{
125 Width: 1260,
126 Height: 720,
127 },
128 InputDeviceMappings: []v2.InputDeviceName{
129 eloTouchSolutionsUSB,
130 },
131 },
132 {
133 DisplayPort: card0HDMI2,
134 MPID: &ncr22888,
135 Primary: ¬Primary,
136 Orientation: &v2.LeftOrientation,
137 Resolution: &v2.Resolution{
138 Width: 1260,
139 Height: 720,
140 },
141 InputDeviceMappings: []v2.InputDeviceName{
142 iSolutionMultiTouch,
143 },
144 },
145 },
146 DPMS: &v2.DPMS{
147 Enabled: &dpmsEnabled,
148 BlankTime: &zeroSeconds,
149 StandbyTime: &zeroSeconds,
150 SuspendTime: &sixtySeconds,
151 OffTime: &sixtySeconds,
152 },
153 Layout: v2.Layout{card0DP1, card0HDMI2, card0HDMI1},
154 }
155
156 func TestController_ReconcileNodeDisplayConfig(t *testing.T) {
157 var (
158 nodeDisplayConfig *v2.NodeDisplayConfig
159 node *corev1.Node
160 )
161
162 status := f2.NewFeature("Reconcile NodeDisplayConfig").
163 Test("Default configuration is applied", func(ctx f2.Context, t *testing.T) f2.Context {
164 k := ktest.FromContextT(ctx, t)
165
166 nodeDisplayConfig = &v2.NodeDisplayConfig{
167 ObjectMeta: metav1.ObjectMeta{
168 Name: displayctlHostname,
169 },
170 }
171
172 node = &corev1.Node{
173 ObjectMeta: metav1.ObjectMeta{
174 Name: displayctlHostname,
175 },
176 }
177
178 now := time.Now()
179 time.Sleep(time.Second)
180
181 require.NoError(t, k.Client.Create(ctx, nodeDisplayConfig))
182 require.NoError(t, k.Client.Create(ctx, node))
183
184 k.WaitOn(t, k.Check(nodeDisplayConfig, configurationIsAppliedAfter(now)))
185
186 require.Equal(t, expectedDefaultDisplayConfig, nodeDisplayConfig.AppliedDisplayConfig(), "expected default configuration was not applied")
187 require.True(t, conditions.IsReady(nodeDisplayConfig), "NodeDisplayConfig is not ready")
188 require.True(t, conditions.IsTrue(nodeDisplayConfig, v2.DisplayManagerConfiguredCondition), "DisplayManagerConfigured condition not true")
189 require.True(t, conditions.IsTrue(nodeDisplayConfig, v2.DefaultCondition), "was not marked as default")
190 require.True(t, conditions.IsTrue(nodeDisplayConfig, v2.DisplayctlEnabledCondition), "displayctl should be enabled")
191 require.True(t, conditions.IsFalse(nodeDisplayConfig, v2.DisplayManagerConfigCondition), "display manager config should not exist")
192
193 require.NoError(t, k.Client.Get(ctx, client.ObjectKey{Name: displayctlHostname}, node))
194 require.Contains(t, node.Status.Capacity, corev1.ResourceName(resource.UIRequestResource), "ui-request node resource not set")
195 require.True(t, node.Status.Capacity[resource.UIRequestResource.ResourceName()].Equal(kresource.MustParse("1000")), "ui-request node resource not set to 1k")
196
197 return ctx
198 }).
199 Test("Custom configuration is applied", func(ctx f2.Context, t *testing.T) f2.Context {
200 k := ktest.FromContextT(ctx, t)
201
202 require.NoError(t, k.Client.Get(ctx, client.ObjectKey{Name: displayctlHostname}, nodeDisplayConfig))
203
204 nodeDisplayConfig.Spec = &v2.DisplayConfig{
205 Displays: []v2.Display{
206 {
207 DisplayPort: card0HDMI2,
208 Primary: ¬Primary,
209 Orientation: &v2.LeftOrientation,
210 Resolution: &v2.Resolution{
211 Width: 1260,
212 Height: 720,
213 },
214 InputDeviceMappings: []v2.InputDeviceName{
215 iSolutionMultiTouch,
216 },
217 },
218 {
219 DisplayPort: card0DP1,
220 Primary: &primary,
221 InputDeviceMappings: []v2.InputDeviceName{
222 iSolutionMultiTouch,
223 },
224 },
225 },
226 DPMS: &v2.DPMS{
227 Enabled: &dpmsEnabled,
228 SuspendTime: &sixtySeconds,
229 OffTime: &sixtySeconds,
230 },
231 Layout: v2.Layout{card0DP1, card0HDMI2, card0HDMI1},
232 }
233
234 now := time.Now()
235 time.Sleep(time.Second)
236
237 require.NoError(t, k.Client.Update(ctx, nodeDisplayConfig))
238 k.WaitOn(t, k.Check(nodeDisplayConfig, configurationIsAppliedAfter(now)))
239
240 require.Equal(t, expectedCustomDisplayConfig, nodeDisplayConfig.AppliedDisplayConfig(), "expected custom configuration was not applied")
241 require.True(t, conditions.IsReady(nodeDisplayConfig), "NodeDisplayConfig is not ready")
242 require.True(t, conditions.IsTrue(nodeDisplayConfig, v2.DisplayManagerConfiguredCondition), "DisplayManagerConfigured condition not true")
243 require.True(t, conditions.IsFalse(nodeDisplayConfig, v2.DefaultCondition), "was not marked as custom")
244
245 return ctx
246 }).
247 Test("Displays not present on node are not configured", func(ctx f2.Context, t *testing.T) f2.Context {
248 k := ktest.FromContextT(ctx, t)
249
250 require.NoError(t, k.Client.Get(ctx, client.ObjectKey{Name: displayctlHostname}, nodeDisplayConfig))
251
252
253 nodeDisplayConfig.Spec = &v2.DisplayConfig{
254 Displays: []v2.Display{
255 {
256 DisplayPort: card0DP2,
257 Orientation: &v2.NormalOrientation,
258 },
259 },
260 }
261
262 now := time.Now()
263 time.Sleep(time.Second)
264
265 require.NoError(t, k.Client.Update(ctx, nodeDisplayConfig))
266 k.WaitOn(t, k.Check(nodeDisplayConfig, configurationIsAppliedAfter(now)))
267
268
269 appliedDisplayConfig := nodeDisplayConfig.AppliedDisplayConfig()
270 require.Nil(t, appliedDisplayConfig.Displays.FindByDisplayPort(card0DP2), "DP2 should not be present in applied displays")
271 require.True(t, conditions.IsTrue(nodeDisplayConfig, v2.DisplayManagerConfiguredCondition), "DisplayManagerConfigured condition not true")
272 require.True(t, conditions.IsReady(nodeDisplayConfig), "NodeDisplayConfig is not ready")
273
274 return ctx
275 }).
276 Test("Removing spec resets config to defaults", func(ctx f2.Context, t *testing.T) f2.Context {
277 k := ktest.FromContextT(ctx, t)
278
279 require.NoError(t, k.Client.Get(ctx, client.ObjectKey{Name: displayctlHostname}, nodeDisplayConfig))
280
281 nodeDisplayConfig.Spec = nil
282
283 now := time.Now()
284 time.Sleep(time.Second)
285
286 require.NoError(t, k.Client.Update(ctx, nodeDisplayConfig))
287 k.WaitOn(t, k.Check(nodeDisplayConfig, configurationIsAppliedAfter(now)))
288
289 require.Equal(t, expectedDefaultDisplayConfig, nodeDisplayConfig.AppliedDisplayConfig(), "expected default configuration was not applied")
290 require.True(t, conditions.IsTrue(nodeDisplayConfig, v2.DisplayManagerConfiguredCondition), "DisplayManagerConfigured condition not true")
291 require.True(t, conditions.IsReady(nodeDisplayConfig), "NodeDisplayConfig is not ready")
292
293 return ctx
294 }).
295 Test("Display manager watcher triggers reconcile", func(ctx f2.Context, t *testing.T) f2.Context {
296 k := ktest.FromContextT(ctx, t)
297
298 require.NoError(t, k.Client.Get(ctx, client.ObjectKey{Name: displayctlHostname}, nodeDisplayConfig))
299
300 if nodeDisplayConfig.Annotations == nil {
301 nodeDisplayConfig.Annotations = map[string]string{}
302 }
303
304 restartedAt := time.Now()
305 time.Sleep(time.Second)
306
307
308 nodeDisplayConfig.Annotations[v2.DisplayManagerRestartedAtAnnotation] = restartedAt.Format(time.RFC3339)
309
310 require.NoError(t, k.Client.Update(ctx, nodeDisplayConfig))
311 k.WaitOn(t, k.Check(nodeDisplayConfig, configurationIsAppliedAfter(restartedAt)))
312
313 return ctx
314 }).
315 Test("Device watcher triggers reconcile", func(ctx f2.Context, t *testing.T) f2.Context {
316 k := ktest.FromContextT(ctx, t)
317
318 require.NoError(t, k.Client.Get(ctx, client.ObjectKey{Name: displayctlHostname}, nodeDisplayConfig))
319
320 restartedAt := time.Now()
321 time.Sleep(time.Second)
322
323
324 nodeDisplayConfig.Annotations[v2.DevicesUpdatedAtAnnotation] = restartedAt.Format(time.RFC3339)
325
326 require.NoError(t, k.Client.Update(ctx, nodeDisplayConfig))
327 k.WaitOn(t, k.Check(nodeDisplayConfig, configurationIsAppliedAfter(restartedAt)))
328
329 return ctx
330 }).
331 Test("Displayctl can be disabled", func(ctx f2.Context, t *testing.T) f2.Context {
332 k := ktest.FromContextT(ctx, t)
333
334 require.NoError(t, k.Client.Get(ctx, client.ObjectKey{Name: displayctlHostname}, nodeDisplayConfig))
335
336
337 config := xserverconfig.New(displayctlHostname)
338 config.SetDisplayctlEnabled(false)
339 require.NoError(t, config.UpdateConfigMap(ctx, k.Client))
340
341 k.WaitOn(t, k.Check(nodeDisplayConfig, displayctlIsEnabled(false)))
342
343 require.True(t, conditions.IsTrue(nodeDisplayConfig, v2.DisplayManagerConfigCondition), "display manager config should exist")
344
345 return ctx
346 }).
347 Test("Displayctl can be re-enabled", func(ctx f2.Context, t *testing.T) f2.Context {
348 k := ktest.FromContextT(ctx, t)
349
350
351 config, err := xserverconfig.FromClient(ctx, displayctlHostname, k.Client)
352 require.NoError(t, err)
353
354
355 config.SetDisplayctlEnabled(true)
356 require.NoError(t, config.UpdateConfigMap(ctx, k.Client))
357
358 k.WaitOn(t, k.Check(nodeDisplayConfig, displayctlIsEnabled(true)))
359
360 return ctx
361 }).
362 Test("NodeDisplayConfig can be deleted", func(ctx f2.Context, t *testing.T) f2.Context {
363 k := ktest.FromContextT(ctx, t)
364 require.NoError(t, k.Client.Delete(ctx, nodeDisplayConfig))
365 k.WaitOn(t, k.ObjDeleted(nodeDisplayConfig))
366 return ctx
367 }).
368 Serial().
369 Feature()
370
371 f.Test(t, status)
372 }
373
374 func configurationIsAppliedAfter(after time.Time) kmp.Komparison {
375 return func(obj client.Object) cmp.Result {
376 nodeDisplayConfig, ok := obj.(*v2.NodeDisplayConfig)
377 if !ok {
378 return cmp.ResultFromError(errCouldNotCastObjectToNodeDisplayConfig)
379 }
380
381 if nodeDisplayConfig.Status.Applied.LastAppliedTimestamp.IsZero() {
382 return cmp.ResultFailure("status has not been updated")
383 }
384
385
386 if nodeDisplayConfig.Status.Applied.LastAppliedTimestamp.After(after) {
387 return cmp.ResultSuccess
388 }
389 return cmp.ResultFailure(fmt.Sprintf(
390 "config was not applied after expected time: expected after %s, but last applied at %s",
391 after.Format(time.RFC3339),
392 nodeDisplayConfig.Status.Applied.LastAppliedTimestamp.Format(time.RFC3339),
393 ))
394 }
395 }
396
397 func displayctlIsEnabled(enabled bool) kmp.Komparison {
398 return func(obj client.Object) cmp.Result {
399 nodeDisplayConfig, ok := obj.(*v2.NodeDisplayConfig)
400 if !ok {
401 return cmp.ResultFromError(errCouldNotCastObjectToNodeDisplayConfig)
402 }
403
404
405 if enabled {
406 if conditions.IsTrue(nodeDisplayConfig, v2.DisplayctlEnabledCondition) {
407 return cmp.ResultSuccess
408 }
409 return cmp.ResultFailure("displayctl was not enabled")
410 }
411
412
413 if conditions.IsFalse(nodeDisplayConfig, v2.DisplayctlEnabledCondition) {
414 return cmp.ResultSuccess
415 }
416 return cmp.ResultFailure("displayctl was not disabled")
417 }
418 }
419
View as plain text