package integration import ( "fmt" "testing" "time" "github.com/stretchr/testify/require" "gotest.tools/v3/assert/cmp" corev1 "k8s.io/api/core/v1" kresource "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" "edge-infra.dev/pkg/k8s/runtime/conditions" "edge-infra.dev/pkg/k8s/testing/kmp" v2 "edge-infra.dev/pkg/sds/display/k8s/apis/v2" xserverconfig "edge-infra.dev/pkg/sds/display/k8s/controllers/xserver/config" "edge-infra.dev/pkg/sds/ien/resource" "edge-infra.dev/test/f2" "edge-infra.dev/test/f2/x/ktest" ) var displayctlHostname = "displayctl-host" var ( card0HDMI1 = v2.DisplayPort("card0-HDMI1") card0HDMI2 = v2.DisplayPort("card0-HDMI2") card0DP1 = v2.DisplayPort("card0-DP1") card0DP2 = v2.DisplayPort("card0-DP2") elo4098 = v2.MPID("ELO-4098") ncr22888 = v2.MPID("NCR-22888") vsc9365 = v2.MPID("VSC-9365") eloTouchSolutionsUSB = v2.InputDeviceName("Elo Touch Solutions Elo Touch Solutions Pcap USB Interface") eGalaxEXC3189 = v2.InputDeviceName("eGalax Inc. eGalaxTouch EXC3189-2506-09.00.00.00") eGalaxEXC3189Mouse = v2.InputDeviceName("eGalax Inc. eGalaxTouch EXC3189-2506-09.00.00.00 Mouse") iSolutionMultiTouch = v2.InputDeviceName("iSolution multitouch") primary = v2.Primary(true) notPrimary = v2.Primary(false) dpmsEnabled = true dpmsDisabled = false zeroSeconds = 0 sixtySeconds = 60 ) var errCouldNotCastObjectToNodeDisplayConfig = fmt.Errorf("could not cast object to *v2.NodeDisplayConfig") var expectedDefaultDisplayConfig = &v2.DisplayConfig{ Displays: []v2.Display{ { DisplayPort: card0DP1, MPID: &vsc9365, Primary: ¬Primary, Orientation: &v2.NormalOrientation, Resolution: &v2.Resolution{ Width: 1280, Height: 1084, }, }, { DisplayPort: card0HDMI1, MPID: &elo4098, Primary: &primary, Orientation: &v2.NormalOrientation, Resolution: &v2.Resolution{ Width: 1260, Height: 720, }, InputDeviceMappings: []v2.InputDeviceName{ eloTouchSolutionsUSB, }, }, { DisplayPort: card0HDMI2, MPID: &ncr22888, Primary: ¬Primary, Orientation: &v2.NormalOrientation, Resolution: &v2.Resolution{ Width: 1920, Height: 1080, }, InputDeviceMappings: []v2.InputDeviceName{ eGalaxEXC3189, eGalaxEXC3189Mouse, }, }, }, // DPMS is overwritten from reader by defaults DPMS: &v2.DPMS{ Enabled: &dpmsDisabled, BlankTime: &zeroSeconds, StandbyTime: &zeroSeconds, SuspendTime: &zeroSeconds, OffTime: &zeroSeconds, }, Layout: v2.Layout{card0HDMI1, card0HDMI2, card0DP1}, } var expectedCustomDisplayConfig = &v2.DisplayConfig{ Displays: []v2.Display{ { DisplayPort: card0DP1, MPID: &vsc9365, Primary: &primary, Orientation: &v2.NormalOrientation, Resolution: &v2.Resolution{ Width: 1280, Height: 1084, }, InputDeviceMappings: []v2.InputDeviceName{ iSolutionMultiTouch, }, }, { DisplayPort: card0HDMI1, MPID: &elo4098, Primary: ¬Primary, Orientation: &v2.NormalOrientation, Resolution: &v2.Resolution{ Width: 1260, Height: 720, }, InputDeviceMappings: []v2.InputDeviceName{ eloTouchSolutionsUSB, }, }, { DisplayPort: card0HDMI2, MPID: &ncr22888, Primary: ¬Primary, Orientation: &v2.LeftOrientation, Resolution: &v2.Resolution{ Width: 1260, Height: 720, }, InputDeviceMappings: []v2.InputDeviceName{ iSolutionMultiTouch, }, }, }, DPMS: &v2.DPMS{ Enabled: &dpmsEnabled, BlankTime: &zeroSeconds, StandbyTime: &zeroSeconds, SuspendTime: &sixtySeconds, OffTime: &sixtySeconds, }, Layout: v2.Layout{card0DP1, card0HDMI2, card0HDMI1}, } func TestController_ReconcileNodeDisplayConfig(t *testing.T) { var ( nodeDisplayConfig *v2.NodeDisplayConfig node *corev1.Node ) status := f2.NewFeature("Reconcile NodeDisplayConfig"). Test("Default configuration is applied", func(ctx f2.Context, t *testing.T) f2.Context { k := ktest.FromContextT(ctx, t) nodeDisplayConfig = &v2.NodeDisplayConfig{ ObjectMeta: metav1.ObjectMeta{ Name: displayctlHostname, }, } node = &corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: displayctlHostname, }, } now := time.Now() time.Sleep(time.Second) require.NoError(t, k.Client.Create(ctx, nodeDisplayConfig)) require.NoError(t, k.Client.Create(ctx, node)) k.WaitOn(t, k.Check(nodeDisplayConfig, configurationIsAppliedAfter(now))) require.Equal(t, expectedDefaultDisplayConfig, nodeDisplayConfig.AppliedDisplayConfig(), "expected default configuration was not applied") require.True(t, conditions.IsReady(nodeDisplayConfig), "NodeDisplayConfig is not ready") require.True(t, conditions.IsTrue(nodeDisplayConfig, v2.DisplayManagerConfiguredCondition), "DisplayManagerConfigured condition not true") require.True(t, conditions.IsTrue(nodeDisplayConfig, v2.DefaultCondition), "was not marked as default") require.True(t, conditions.IsTrue(nodeDisplayConfig, v2.DisplayctlEnabledCondition), "displayctl should be enabled") require.True(t, conditions.IsFalse(nodeDisplayConfig, v2.DisplayManagerConfigCondition), "display manager config should not exist") require.NoError(t, k.Client.Get(ctx, client.ObjectKey{Name: displayctlHostname}, node)) require.Contains(t, node.Status.Capacity, corev1.ResourceName(resource.UIRequestResource), "ui-request node resource not set") require.True(t, node.Status.Capacity[resource.UIRequestResource.ResourceName()].Equal(kresource.MustParse("1000")), "ui-request node resource not set to 1k") return ctx }). Test("Custom configuration is applied", func(ctx f2.Context, t *testing.T) f2.Context { k := ktest.FromContextT(ctx, t) require.NoError(t, k.Client.Get(ctx, client.ObjectKey{Name: displayctlHostname}, nodeDisplayConfig)) nodeDisplayConfig.Spec = &v2.DisplayConfig{ Displays: []v2.Display{ { DisplayPort: card0HDMI2, Primary: ¬Primary, Orientation: &v2.LeftOrientation, Resolution: &v2.Resolution{ Width: 1260, Height: 720, }, InputDeviceMappings: []v2.InputDeviceName{ iSolutionMultiTouch, }, }, { DisplayPort: card0DP1, Primary: &primary, InputDeviceMappings: []v2.InputDeviceName{ iSolutionMultiTouch, }, }, }, DPMS: &v2.DPMS{ Enabled: &dpmsEnabled, SuspendTime: &sixtySeconds, OffTime: &sixtySeconds, }, Layout: v2.Layout{card0DP1, card0HDMI2, card0HDMI1}, } now := time.Now() time.Sleep(time.Second) require.NoError(t, k.Client.Update(ctx, nodeDisplayConfig)) k.WaitOn(t, k.Check(nodeDisplayConfig, configurationIsAppliedAfter(now))) require.Equal(t, expectedCustomDisplayConfig, nodeDisplayConfig.AppliedDisplayConfig(), "expected custom configuration was not applied") require.True(t, conditions.IsReady(nodeDisplayConfig), "NodeDisplayConfig is not ready") require.True(t, conditions.IsTrue(nodeDisplayConfig, v2.DisplayManagerConfiguredCondition), "DisplayManagerConfigured condition not true") require.True(t, conditions.IsFalse(nodeDisplayConfig, v2.DefaultCondition), "was not marked as custom") return ctx }). Test("Displays not present on node are not configured", func(ctx f2.Context, t *testing.T) f2.Context { k := ktest.FromContextT(ctx, t) require.NoError(t, k.Client.Get(ctx, client.ObjectKey{Name: displayctlHostname}, nodeDisplayConfig)) // "DP2" is not present on host nodeDisplayConfig.Spec = &v2.DisplayConfig{ Displays: []v2.Display{ { DisplayPort: card0DP2, Orientation: &v2.NormalOrientation, }, }, } now := time.Now() time.Sleep(time.Second) require.NoError(t, k.Client.Update(ctx, nodeDisplayConfig)) k.WaitOn(t, k.Check(nodeDisplayConfig, configurationIsAppliedAfter(now))) // NCR-1234 should not be present in the applied display configuration appliedDisplayConfig := nodeDisplayConfig.AppliedDisplayConfig() require.Nil(t, appliedDisplayConfig.Displays.FindByDisplayPort(card0DP2), "DP2 should not be present in applied displays") require.True(t, conditions.IsTrue(nodeDisplayConfig, v2.DisplayManagerConfiguredCondition), "DisplayManagerConfigured condition not true") require.True(t, conditions.IsReady(nodeDisplayConfig), "NodeDisplayConfig is not ready") return ctx }). Test("Removing spec resets config to defaults", func(ctx f2.Context, t *testing.T) f2.Context { k := ktest.FromContextT(ctx, t) require.NoError(t, k.Client.Get(ctx, client.ObjectKey{Name: displayctlHostname}, nodeDisplayConfig)) nodeDisplayConfig.Spec = nil now := time.Now() time.Sleep(time.Second) require.NoError(t, k.Client.Update(ctx, nodeDisplayConfig)) k.WaitOn(t, k.Check(nodeDisplayConfig, configurationIsAppliedAfter(now))) require.Equal(t, expectedDefaultDisplayConfig, nodeDisplayConfig.AppliedDisplayConfig(), "expected default configuration was not applied") require.True(t, conditions.IsTrue(nodeDisplayConfig, v2.DisplayManagerConfiguredCondition), "DisplayManagerConfigured condition not true") require.True(t, conditions.IsReady(nodeDisplayConfig), "NodeDisplayConfig is not ready") return ctx }). Test("Display manager watcher triggers reconcile", func(ctx f2.Context, t *testing.T) f2.Context { k := ktest.FromContextT(ctx, t) require.NoError(t, k.Client.Get(ctx, client.ObjectKey{Name: displayctlHostname}, nodeDisplayConfig)) if nodeDisplayConfig.Annotations == nil { nodeDisplayConfig.Annotations = map[string]string{} } restartedAt := time.Now() time.Sleep(time.Second) // update the annotation to simulate display manager restart nodeDisplayConfig.Annotations[v2.DisplayManagerRestartedAtAnnotation] = restartedAt.Format(time.RFC3339) require.NoError(t, k.Client.Update(ctx, nodeDisplayConfig)) k.WaitOn(t, k.Check(nodeDisplayConfig, configurationIsAppliedAfter(restartedAt))) return ctx }). Test("Device watcher triggers reconcile", func(ctx f2.Context, t *testing.T) f2.Context { k := ktest.FromContextT(ctx, t) require.NoError(t, k.Client.Get(ctx, client.ObjectKey{Name: displayctlHostname}, nodeDisplayConfig)) restartedAt := time.Now() time.Sleep(time.Second) // update the annotation to simulate new device appearing nodeDisplayConfig.Annotations[v2.DevicesUpdatedAtAnnotation] = restartedAt.Format(time.RFC3339) require.NoError(t, k.Client.Update(ctx, nodeDisplayConfig)) k.WaitOn(t, k.Check(nodeDisplayConfig, configurationIsAppliedAfter(restartedAt))) return ctx }). Test("Displayctl can be disabled", func(ctx f2.Context, t *testing.T) f2.Context { k := ktest.FromContextT(ctx, t) require.NoError(t, k.Client.Get(ctx, client.ObjectKey{Name: displayctlHostname}, nodeDisplayConfig)) // create new xserver config with displayctl disabled config := xserverconfig.New(displayctlHostname) config.SetDisplayctlEnabled(false) require.NoError(t, config.UpdateConfigMap(ctx, k.Client)) k.WaitOn(t, k.Check(nodeDisplayConfig, displayctlIsEnabled(false))) require.True(t, conditions.IsTrue(nodeDisplayConfig, v2.DisplayManagerConfigCondition), "display manager config should exist") return ctx }). Test("Displayctl can be re-enabled", func(ctx f2.Context, t *testing.T) f2.Context { k := ktest.FromContextT(ctx, t) // fetch the existing xserver config config, err := xserverconfig.FromClient(ctx, displayctlHostname, k.Client) require.NoError(t, err) // re-enable displayctl config.SetDisplayctlEnabled(true) require.NoError(t, config.UpdateConfigMap(ctx, k.Client)) k.WaitOn(t, k.Check(nodeDisplayConfig, displayctlIsEnabled(true))) return ctx }). Test("NodeDisplayConfig can be deleted", func(ctx f2.Context, t *testing.T) f2.Context { k := ktest.FromContextT(ctx, t) require.NoError(t, k.Client.Delete(ctx, nodeDisplayConfig)) k.WaitOn(t, k.ObjDeleted(nodeDisplayConfig)) return ctx }). Serial(). Feature() f.Test(t, status) } func configurationIsAppliedAfter(after time.Time) kmp.Komparison { return func(obj client.Object) cmp.Result { nodeDisplayConfig, ok := obj.(*v2.NodeDisplayConfig) if !ok { return cmp.ResultFromError(errCouldNotCastObjectToNodeDisplayConfig) } if nodeDisplayConfig.Status.Applied.LastAppliedTimestamp.IsZero() { return cmp.ResultFailure("status has not been updated") } // check configuration was applied after timestamp if nodeDisplayConfig.Status.Applied.LastAppliedTimestamp.After(after) { return cmp.ResultSuccess } return cmp.ResultFailure(fmt.Sprintf( "config was not applied after expected time: expected after %s, but last applied at %s", after.Format(time.RFC3339), nodeDisplayConfig.Status.Applied.LastAppliedTimestamp.Format(time.RFC3339), )) } } func displayctlIsEnabled(enabled bool) kmp.Komparison { return func(obj client.Object) cmp.Result { nodeDisplayConfig, ok := obj.(*v2.NodeDisplayConfig) if !ok { return cmp.ResultFromError(errCouldNotCastObjectToNodeDisplayConfig) } // check if displayctl is enabled if enabled { if conditions.IsTrue(nodeDisplayConfig, v2.DisplayctlEnabledCondition) { return cmp.ResultSuccess } return cmp.ResultFailure("displayctl was not enabled") } // otherwise ensure it was disabled if conditions.IsFalse(nodeDisplayConfig, v2.DisplayctlEnabledCondition) { return cmp.ResultSuccess } return cmp.ResultFailure("displayctl was not disabled") } }