...

Source file src/edge-infra.dev/pkg/sds/display/k8s/controllers/displayctl/integration/nodedisplayconfig_controller_test.go

Documentation: edge-infra.dev/pkg/sds/display/k8s/controllers/displayctl/integration

     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:     &notPrimary,
    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:     &notPrimary,
    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  	// DPMS is overwritten from reader by defaults
    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:     &notPrimary,
   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:     &notPrimary,
   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:     &notPrimary,
   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  			// "DP2" is not present on host
   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  			// NCR-1234 should not be present in the applied display configuration
   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  			// update the annotation to simulate display manager restart
   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  			// update the annotation to simulate new device appearing
   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  			// create new xserver config with displayctl disabled
   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  			// fetch the existing xserver config
   351  			config, err := xserverconfig.FromClient(ctx, displayctlHostname, k.Client)
   352  			require.NoError(t, err)
   353  
   354  			// re-enable displayctl
   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  		// check configuration was applied after timestamp
   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  		// check if displayctl is enabled
   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  		// otherwise ensure it was disabled
   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