...

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

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

     1  package displayctl
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  
     8  	"github.com/go-logr/logr"
     9  	corev1 "k8s.io/api/core/v1"
    10  	kerrors "k8s.io/apimachinery/pkg/api/errors"
    11  	ctrl "sigs.k8s.io/controller-runtime"
    12  	"sigs.k8s.io/controller-runtime/pkg/builder"
    13  	"sigs.k8s.io/controller-runtime/pkg/client"
    14  	"sigs.k8s.io/controller-runtime/pkg/event"
    15  	"sigs.k8s.io/controller-runtime/pkg/handler"
    16  	"sigs.k8s.io/controller-runtime/pkg/predicate"
    17  	ctrlreconcile "sigs.k8s.io/controller-runtime/pkg/reconcile"
    18  
    19  	"edge-infra.dev/pkg/k8s/meta/status"
    20  	"edge-infra.dev/pkg/k8s/runtime/controller/reconcile"
    21  	"edge-infra.dev/pkg/k8s/runtime/patch"
    22  	"edge-infra.dev/pkg/sds/display/constants"
    23  	"edge-infra.dev/pkg/sds/display/displaymanager/manager"
    24  	v2 "edge-infra.dev/pkg/sds/display/k8s/apis/v2"
    25  	"edge-infra.dev/pkg/sds/display/k8s/controllers/displayctl/internal/displayconfig"
    26  	"edge-infra.dev/pkg/sds/display/k8s/controllers/displayctl/internal/metrics"
    27  	xserverconfig "edge-infra.dev/pkg/sds/display/k8s/controllers/xserver/config"
    28  	"edge-infra.dev/pkg/sds/ien/resource"
    29  )
    30  
    31  const successResourceValue = "1000"
    32  
    33  // NodeDisplayConfigController reconciles the host's NodeDisplayConfig to configure its displays.
    34  type NodeDisplayConfigController struct {
    35  	Name    string
    36  	Client  client.Client
    37  	Metrics metrics.Metrics
    38  
    39  	manager.DisplayManager
    40  }
    41  
    42  func NewNodeDisplayConfigController(displayManager manager.DisplayManager, mgr ctrl.Manager) *NodeDisplayConfigController {
    43  	return &NodeDisplayConfigController{
    44  		Name:           constants.NodeDisplayConfigControllerName,
    45  		Client:         mgr.GetClient(),
    46  		Metrics:        *metrics.New(mgr, constants.DisplayctlName),
    47  		DisplayManager: displayManager,
    48  	}
    49  }
    50  
    51  func (c *NodeDisplayConfigController) SetupWithManager(mgr ctrl.Manager) error {
    52  	return ctrl.NewControllerManagedBy(mgr).
    53  		For(&v2.NodeDisplayConfig{}, nodeDisplayConfigPredicates(c.Hostname())).
    54  		Watches(
    55  			// reconcile on changes to xserver-config or display-port-override ConfigMaps
    56  			&corev1.ConfigMap{},
    57  			handler.EnqueueRequestsFromMapFunc(c.createReconcileRequests),
    58  			configMapPredicates(c.Hostname()),
    59  		).
    60  		WithEventFilter(createEventFilter(true, true, true)).
    61  		Complete(c)
    62  }
    63  
    64  func nodeDisplayConfigPredicates(hostname string) builder.Predicates {
    65  	return builder.WithPredicates(
    66  		isHostNodeDisplayConfigPredicate(hostname),
    67  		predicate.Or(
    68  			predicate.GenerationChangedPredicate{},
    69  			annotationChangedPredicate(v2.DisplayManagerRestartedAtAnnotation, v2.DevicesUpdatedAtAnnotation),
    70  		),
    71  	)
    72  }
    73  
    74  func isHostNodeDisplayConfigPredicate(hostname string) predicate.Predicate {
    75  	return predicate.NewPredicateFuncs(func(obj client.Object) bool {
    76  		return obj.GetName() == hostname
    77  	})
    78  }
    79  
    80  func annotationChangedPredicate(annos ...string) predicate.Predicate {
    81  	return predicate.Funcs{
    82  		UpdateFunc: func(e event.UpdateEvent) bool {
    83  			oldAnnos := e.ObjectOld.GetAnnotations()
    84  			newAnnos := e.ObjectNew.GetAnnotations()
    85  			for _, anno := range annos {
    86  				if oldAnnos[anno] != newAnnos[anno] {
    87  					return true
    88  				}
    89  			}
    90  			return false
    91  		},
    92  	}
    93  }
    94  
    95  func configMapPredicates(hostname string) builder.Predicates {
    96  	return builder.WithPredicates(
    97  		predicate.Or(
    98  			isHostXSeverConfigMapPredicate(hostname),
    99  			isDisplayPortOverrideConfigMapPredicate(),
   100  		),
   101  	)
   102  }
   103  
   104  func isHostXSeverConfigMapPredicate(hostname string) predicate.Funcs {
   105  	return predicate.NewPredicateFuncs(func(obj client.Object) bool {
   106  		return obj.GetNamespace() == constants.Namespace && obj.GetName() == xserverconfig.ConfigMapNameFromHostname(hostname)
   107  	})
   108  }
   109  
   110  func isDisplayPortOverrideConfigMapPredicate() predicate.Funcs {
   111  	return predicate.NewPredicateFuncs(func(obj client.Object) bool {
   112  		return obj.GetNamespace() == constants.Namespace && obj.GetName() == constants.DisplayPortOverride
   113  	})
   114  }
   115  
   116  // Returns a reconcile request for the host NodeDisplayConfig, if it exists
   117  func (c *NodeDisplayConfigController) createReconcileRequests(ctx context.Context, _ client.Object) []ctrlreconcile.Request {
   118  	if err := c.Client.Get(ctx, client.ObjectKey{Name: c.Hostname()}, &v2.NodeDisplayConfig{}); err != nil {
   119  		return nil
   120  	}
   121  	return []ctrlreconcile.Request{
   122  		{NamespacedName: client.ObjectKey{Name: c.Hostname()}},
   123  	}
   124  }
   125  
   126  func createEventFilter(create, update, delete bool) predicate.Predicate {
   127  	return predicate.Funcs{
   128  		CreateFunc: func(event.CreateEvent) bool {
   129  			return create
   130  		},
   131  		UpdateFunc: func(event.UpdateEvent) bool {
   132  			return update
   133  		},
   134  		DeleteFunc: func(event.DeleteEvent) bool {
   135  			return delete
   136  		},
   137  	}
   138  }
   139  
   140  // +kubebuilder:rbac:groups=display.edge.ncr.com,resources=nodedisplayconfigs,verbs=create;get;list;watch;update;patch
   141  // +kubebuilder:rbac:groups=display.edge.ncr.com,resources=nodedisplayconfigs/status,verbs=get;update;patch
   142  // +kubebuilder:rbac:groups="",resources=nodes,verbs=get;list;watch
   143  // +kubebuilder:rbac:groups="",resources=nodes/status,verbs=get;update;patch
   144  // +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch
   145  
   146  func (c *NodeDisplayConfigController) Reconcile(ctx context.Context, req ctrl.Request) (ctrlResult ctrl.Result, err error) {
   147  	log := ctrl.LoggerFrom(ctx).WithName(c.Name)
   148  	ctx = ctrl.LoggerInto(ctx, log)
   149  
   150  	// get the reconciled NodeDisplayConfig
   151  	nodeDisplayConfig := &v2.NodeDisplayConfig{}
   152  	err = c.Client.Get(ctx, req.NamespacedName, nodeDisplayConfig)
   153  	if client.IgnoreNotFound(err) != nil {
   154  		return ctrl.Result{}, err
   155  	}
   156  
   157  	// if the NodeDisplayConfig was deleted, remove the custom configuration
   158  	if kerrors.IsNotFound(err) {
   159  		if err := c.cleanupNodeDisplayConfig(ctx); err != nil {
   160  			return ctrl.Result{}, err
   161  		}
   162  		log.Info("NodeDisplayConfig has been deleted, reverted to default configuration")
   163  		return ctrl.Result{}, nil
   164  	}
   165  
   166  	patcher := patch.NewSerialPatcher(nodeDisplayConfig, c.Client)
   167  	result := reconcile.ResultEmpty
   168  
   169  	// find if displayctl is enabled for the node in X server config
   170  	enabled, configName, err := getDisplayctlEnabled(ctx, c.Hostname(), c.Client)
   171  	if err != nil {
   172  		nodeDisplayConfig.SetDisplayManagerConfiguredCondition(false, v2.FailedStatus, err.Error())
   173  		return
   174  	}
   175  
   176  	// reset the status and signal that we are currently reconciling
   177  	nodeDisplayConfig.ResetStatus()
   178  	nodeDisplayConfig.SetDefaultCondition()
   179  	nodeDisplayConfig.SetDisplayctlEnabledCondition(enabled)
   180  	nodeDisplayConfig.SetDisplayManagerConfigCondition(configName)
   181  	nodeDisplayConfig.SetDisplayManagerConfiguredCondition(false, v2.ReconcilingStatus, "reconciling")
   182  
   183  	if err = reconcile.Progressing(ctx, nodeDisplayConfig, patcher); err != nil {
   184  		return ctrl.Result{}, err
   185  	}
   186  
   187  	defer func() {
   188  		// reconcile cleanup tasks: update node resource, summarize conditions and record metrics
   189  		err = errors.Join(err, updateUIRequestNodeResource(ctx, c.Hostname(), err == nil, log, c.Client))
   190  		ctrlResult, err = c.summarizeAndPatch(ctx, nodeDisplayConfig, result, err, patcher)
   191  		c.Metrics.RecordReconcile(nodeDisplayConfig)
   192  	}()
   193  
   194  	c.Metrics.Reconciling()
   195  
   196  	// upgrade NodeDisplayConfig (if required)
   197  	upgraded, err := c.upgradeNodeDisplayConfig(ctx, nodeDisplayConfig)
   198  	if err != nil {
   199  		nodeDisplayConfig.SetDisplayManagerConfiguredCondition(false, v2.FailedStatus, err.Error())
   200  		return
   201  	} else if upgraded {
   202  		log.Info("NodeDisplayConfig spec has been upgraded", "spec", nodeDisplayConfig.Spec, "disconnected-displays", nodeDisplayConfig.DisconnectedDisplayIDs())
   203  		nodeDisplayConfig.SetDisplayManagerConfiguredCondition(false, v2.UpgradingStatus, "upgrading V1 to V2")
   204  		return
   205  	}
   206  
   207  	// if displayctl is disabled: check display manager is running, but do not configure
   208  	if !enabled {
   209  		if err = c.Wait(ctx); err != nil {
   210  			nodeDisplayConfig.SetDisplayManagerConfiguredCondition(false, v2.FailedStatus, err.Error())
   211  			return
   212  		}
   213  		log.Info("displayctl is disabled, skipping display configuration")
   214  		nodeDisplayConfig.SetDisplayManagerConfiguredCondition(true, v2.DisabledStatus, "displayctl disabled")
   215  		result = reconcile.ResultSuccess
   216  		return
   217  	}
   218  
   219  	// configure displays
   220  	appliedDisplayConfig, err := c.applyNodeDisplayConfig(ctx, nodeDisplayConfig)
   221  	if err != nil {
   222  		nodeDisplayConfig.SetDisplayManagerConfiguredCondition(false, v2.FailedStatus, err.Error())
   223  		return
   224  	}
   225  
   226  	logConfigurationWarnings(nodeDisplayConfig.Spec, appliedDisplayConfig, log)
   227  
   228  	// update the applied configuration statuses and set the Configured condition to true
   229  	nodeDisplayConfig.SetAppliedConfigStatuses(appliedDisplayConfig)
   230  	nodeDisplayConfig.SetDisplayManagerConfiguredCondition(true, v2.UpToDateStatus, "displays configured")
   231  
   232  	log.Info("display configuration updated successfully", "applied", appliedDisplayConfig)
   233  	result = reconcile.ResultSuccess
   234  
   235  	return
   236  }
   237  
   238  func (c *NodeDisplayConfigController) cleanupNodeDisplayConfig(ctx context.Context) error {
   239  	if _, err := displayconfig.Apply(ctx, nil, c.DisplayManager); err != nil {
   240  		return fmt.Errorf("failed to cleanup NodeDisplayConfig configuration: %w", err)
   241  	}
   242  	return nil
   243  }
   244  
   245  func updateUIRequestNodeResource(ctx context.Context, hostname string, register bool, log logr.Logger, c client.Client) error {
   246  	if register {
   247  		return resource.RegisterNodeExtendedResource(ctx, hostname, resource.UIRequestResource, successResourceValue, c)
   248  	}
   249  
   250  	// remove node resource and deschedule pods if we want to deregister
   251  	descheduledPods, err := resource.DeregisterNodeExtendedResourceAndDeschedulePods(ctx, hostname, resource.UIRequestResource, c, client.HasLabels{resource.UIRequestResource.String()})
   252  	if err != nil {
   253  		return err
   254  	}
   255  
   256  	log.Info("removed node resource and descheduled any UI pods", "resource", resource.UIRequestResource, "pods", descheduledPods)
   257  	return nil
   258  }
   259  
   260  func (c *NodeDisplayConfigController) summarizeAndPatch(ctx context.Context, nodeDisplayConfig *v2.NodeDisplayConfig, result reconcile.Result, err error, patcher *patch.SerialPatcher) (ctrl.Result, error) {
   261  	s := reconcile.NewSummarizer(patcher)
   262  	return s.SummarizeAndPatch(
   263  		ctx,
   264  		nodeDisplayConfig,
   265  		reconcile.WithResult(result),
   266  		reconcile.WithError(err),
   267  		reconcile.WithIgnoreNotFound(),
   268  		reconcile.WithFieldOwner(c.Name),
   269  		reconcile.WithConditions(reconcile.Conditions{
   270  			Target:    status.ReadyCondition,
   271  			Owned:     []string{v2.DisplayManagerConfiguredCondition},
   272  			Summarize: []string{v2.DisplayManagerConfiguredCondition},
   273  		}),
   274  		reconcile.WithProcessors(
   275  			reconcile.RecordReconcileReq,
   276  			reconcile.RecordResult,
   277  		),
   278  	)
   279  }
   280  
   281  func (c *NodeDisplayConfigController) applyNodeDisplayConfig(ctx context.Context, nodeDisplayConfig *v2.NodeDisplayConfig) (*v2.DisplayConfig, error) {
   282  	appliedDisplayConfig, err := displayconfig.Apply(ctx, nodeDisplayConfig.Spec, c.DisplayManager)
   283  	if err != nil {
   284  		return nil, fmt.Errorf("failed to apply NodeDisplayConfig: %w", err)
   285  	}
   286  	return appliedDisplayConfig, nil
   287  }
   288  
   289  func getDisplayctlEnabled(ctx context.Context, hostname string, c client.Client) (enabled bool, configName string, err error) {
   290  	config, err := xserverconfig.FromClient(ctx, hostname, c)
   291  	if kerrors.IsNotFound(err) {
   292  		return true, "", nil
   293  	} else if err != nil {
   294  		return false, "", err
   295  	}
   296  	return config.DisplayctlEnabled(), config.ConfigMapName(), nil
   297  }
   298  
   299  // Log warnings when configuration may not be applied as expected.
   300  func logConfigurationWarnings(configuredDisplayConfig, appliedDisplayConfig *v2.DisplayConfig, log logr.Logger) {
   301  	if configuredDisplayConfig != nil {
   302  		logDisplayConfigurationWarnings(configuredDisplayConfig.Displays, appliedDisplayConfig.Displays, log)
   303  	}
   304  	logDuplicateInputDeviceMappingWarnings(appliedDisplayConfig.Displays, log)
   305  }
   306  
   307  // Log warning when a display was configured but not applied.
   308  func logDisplayConfigurationWarnings(configuredDisplays, appliedDisplays v2.Displays, log logr.Logger) {
   309  	dps := []v2.DisplayPort{}
   310  	for _, configuredDisplay := range configuredDisplays {
   311  		if appliedDisplay := appliedDisplays.FindByDisplayPort(configuredDisplay.DisplayPort); appliedDisplay == nil {
   312  			dps = append(dps, configuredDisplay.DisplayPort)
   313  		}
   314  	}
   315  
   316  	if len(dps) > 0 {
   317  		log.Info(
   318  			"WARNING: display(s) specified in host NodeDisplayConfig are not connected and will be ignored",
   319  			"displays", dps,
   320  		)
   321  	}
   322  }
   323  
   324  // Log warning when multiple input device mappings reference the same input device name.
   325  func logDuplicateInputDeviceMappingWarnings(appliedDisplays v2.Displays, log logr.Logger) {
   326  	inputDeviceNameMappings := map[v2.InputDeviceName][]v2.DisplayPort{}
   327  	for _, display := range appliedDisplays {
   328  		for _, inputDeviceMapping := range display.InputDeviceMappings {
   329  			inputDeviceNameMappings[inputDeviceMapping] = append(inputDeviceNameMappings[inputDeviceMapping], display.DisplayPort)
   330  		}
   331  	}
   332  
   333  	for inputDeviceName, dps := range inputDeviceNameMappings {
   334  		if len(dps) > 1 {
   335  			log.Info(
   336  				"WARNING: multiple mappings specify the same input device name - input devices may be mapped to the wrong displays",
   337  				"input device name", inputDeviceName,
   338  				"mapped displays", dps,
   339  			)
   340  		}
   341  	}
   342  }
   343  

View as plain text