package displayctl import ( "context" "errors" "fmt" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/predicate" ctrlreconcile "sigs.k8s.io/controller-runtime/pkg/reconcile" "edge-infra.dev/pkg/k8s/meta/status" "edge-infra.dev/pkg/k8s/runtime/controller/reconcile" "edge-infra.dev/pkg/k8s/runtime/patch" "edge-infra.dev/pkg/sds/display/constants" "edge-infra.dev/pkg/sds/display/displaymanager/manager" v2 "edge-infra.dev/pkg/sds/display/k8s/apis/v2" "edge-infra.dev/pkg/sds/display/k8s/controllers/displayctl/internal/displayconfig" "edge-infra.dev/pkg/sds/display/k8s/controllers/displayctl/internal/metrics" xserverconfig "edge-infra.dev/pkg/sds/display/k8s/controllers/xserver/config" "edge-infra.dev/pkg/sds/ien/resource" ) const successResourceValue = "1000" // NodeDisplayConfigController reconciles the host's NodeDisplayConfig to configure its displays. type NodeDisplayConfigController struct { Name string Client client.Client Metrics metrics.Metrics manager.DisplayManager } func NewNodeDisplayConfigController(displayManager manager.DisplayManager, mgr ctrl.Manager) *NodeDisplayConfigController { return &NodeDisplayConfigController{ Name: constants.NodeDisplayConfigControllerName, Client: mgr.GetClient(), Metrics: *metrics.New(mgr, constants.DisplayctlName), DisplayManager: displayManager, } } func (c *NodeDisplayConfigController) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&v2.NodeDisplayConfig{}, nodeDisplayConfigPredicates(c.Hostname())). Watches( // reconcile on changes to xserver-config or display-port-override ConfigMaps &corev1.ConfigMap{}, handler.EnqueueRequestsFromMapFunc(c.createReconcileRequests), configMapPredicates(c.Hostname()), ). WithEventFilter(createEventFilter(true, true, true)). Complete(c) } func nodeDisplayConfigPredicates(hostname string) builder.Predicates { return builder.WithPredicates( isHostNodeDisplayConfigPredicate(hostname), predicate.Or( predicate.GenerationChangedPredicate{}, annotationChangedPredicate(v2.DisplayManagerRestartedAtAnnotation, v2.DevicesUpdatedAtAnnotation), ), ) } func isHostNodeDisplayConfigPredicate(hostname string) predicate.Predicate { return predicate.NewPredicateFuncs(func(obj client.Object) bool { return obj.GetName() == hostname }) } func annotationChangedPredicate(annos ...string) predicate.Predicate { return predicate.Funcs{ UpdateFunc: func(e event.UpdateEvent) bool { oldAnnos := e.ObjectOld.GetAnnotations() newAnnos := e.ObjectNew.GetAnnotations() for _, anno := range annos { if oldAnnos[anno] != newAnnos[anno] { return true } } return false }, } } func configMapPredicates(hostname string) builder.Predicates { return builder.WithPredicates( predicate.Or( isHostXSeverConfigMapPredicate(hostname), isDisplayPortOverrideConfigMapPredicate(), ), ) } func isHostXSeverConfigMapPredicate(hostname string) predicate.Funcs { return predicate.NewPredicateFuncs(func(obj client.Object) bool { return obj.GetNamespace() == constants.Namespace && obj.GetName() == xserverconfig.ConfigMapNameFromHostname(hostname) }) } func isDisplayPortOverrideConfigMapPredicate() predicate.Funcs { return predicate.NewPredicateFuncs(func(obj client.Object) bool { return obj.GetNamespace() == constants.Namespace && obj.GetName() == constants.DisplayPortOverride }) } // Returns a reconcile request for the host NodeDisplayConfig, if it exists func (c *NodeDisplayConfigController) createReconcileRequests(ctx context.Context, _ client.Object) []ctrlreconcile.Request { if err := c.Client.Get(ctx, client.ObjectKey{Name: c.Hostname()}, &v2.NodeDisplayConfig{}); err != nil { return nil } return []ctrlreconcile.Request{ {NamespacedName: client.ObjectKey{Name: c.Hostname()}}, } } func createEventFilter(create, update, delete bool) predicate.Predicate { return predicate.Funcs{ CreateFunc: func(event.CreateEvent) bool { return create }, UpdateFunc: func(event.UpdateEvent) bool { return update }, DeleteFunc: func(event.DeleteEvent) bool { return delete }, } } // +kubebuilder:rbac:groups=display.edge.ncr.com,resources=nodedisplayconfigs,verbs=create;get;list;watch;update;patch // +kubebuilder:rbac:groups=display.edge.ncr.com,resources=nodedisplayconfigs/status,verbs=get;update;patch // +kubebuilder:rbac:groups="",resources=nodes,verbs=get;list;watch // +kubebuilder:rbac:groups="",resources=nodes/status,verbs=get;update;patch // +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch func (c *NodeDisplayConfigController) Reconcile(ctx context.Context, req ctrl.Request) (ctrlResult ctrl.Result, err error) { log := ctrl.LoggerFrom(ctx).WithName(c.Name) ctx = ctrl.LoggerInto(ctx, log) // get the reconciled NodeDisplayConfig nodeDisplayConfig := &v2.NodeDisplayConfig{} err = c.Client.Get(ctx, req.NamespacedName, nodeDisplayConfig) if client.IgnoreNotFound(err) != nil { return ctrl.Result{}, err } // if the NodeDisplayConfig was deleted, remove the custom configuration if kerrors.IsNotFound(err) { if err := c.cleanupNodeDisplayConfig(ctx); err != nil { return ctrl.Result{}, err } log.Info("NodeDisplayConfig has been deleted, reverted to default configuration") return ctrl.Result{}, nil } patcher := patch.NewSerialPatcher(nodeDisplayConfig, c.Client) result := reconcile.ResultEmpty // find if displayctl is enabled for the node in X server config enabled, configName, err := getDisplayctlEnabled(ctx, c.Hostname(), c.Client) if err != nil { nodeDisplayConfig.SetDisplayManagerConfiguredCondition(false, v2.FailedStatus, err.Error()) return } // reset the status and signal that we are currently reconciling nodeDisplayConfig.ResetStatus() nodeDisplayConfig.SetDefaultCondition() nodeDisplayConfig.SetDisplayctlEnabledCondition(enabled) nodeDisplayConfig.SetDisplayManagerConfigCondition(configName) nodeDisplayConfig.SetDisplayManagerConfiguredCondition(false, v2.ReconcilingStatus, "reconciling") if err = reconcile.Progressing(ctx, nodeDisplayConfig, patcher); err != nil { return ctrl.Result{}, err } defer func() { // reconcile cleanup tasks: update node resource, summarize conditions and record metrics err = errors.Join(err, updateUIRequestNodeResource(ctx, c.Hostname(), err == nil, log, c.Client)) ctrlResult, err = c.summarizeAndPatch(ctx, nodeDisplayConfig, result, err, patcher) c.Metrics.RecordReconcile(nodeDisplayConfig) }() c.Metrics.Reconciling() // upgrade NodeDisplayConfig (if required) upgraded, err := c.upgradeNodeDisplayConfig(ctx, nodeDisplayConfig) if err != nil { nodeDisplayConfig.SetDisplayManagerConfiguredCondition(false, v2.FailedStatus, err.Error()) return } else if upgraded { log.Info("NodeDisplayConfig spec has been upgraded", "spec", nodeDisplayConfig.Spec, "disconnected-displays", nodeDisplayConfig.DisconnectedDisplayIDs()) nodeDisplayConfig.SetDisplayManagerConfiguredCondition(false, v2.UpgradingStatus, "upgrading V1 to V2") return } // if displayctl is disabled: check display manager is running, but do not configure if !enabled { if err = c.Wait(ctx); err != nil { nodeDisplayConfig.SetDisplayManagerConfiguredCondition(false, v2.FailedStatus, err.Error()) return } log.Info("displayctl is disabled, skipping display configuration") nodeDisplayConfig.SetDisplayManagerConfiguredCondition(true, v2.DisabledStatus, "displayctl disabled") result = reconcile.ResultSuccess return } // configure displays appliedDisplayConfig, err := c.applyNodeDisplayConfig(ctx, nodeDisplayConfig) if err != nil { nodeDisplayConfig.SetDisplayManagerConfiguredCondition(false, v2.FailedStatus, err.Error()) return } logConfigurationWarnings(nodeDisplayConfig.Spec, appliedDisplayConfig, log) // update the applied configuration statuses and set the Configured condition to true nodeDisplayConfig.SetAppliedConfigStatuses(appliedDisplayConfig) nodeDisplayConfig.SetDisplayManagerConfiguredCondition(true, v2.UpToDateStatus, "displays configured") log.Info("display configuration updated successfully", "applied", appliedDisplayConfig) result = reconcile.ResultSuccess return } func (c *NodeDisplayConfigController) cleanupNodeDisplayConfig(ctx context.Context) error { if _, err := displayconfig.Apply(ctx, nil, c.DisplayManager); err != nil { return fmt.Errorf("failed to cleanup NodeDisplayConfig configuration: %w", err) } return nil } func updateUIRequestNodeResource(ctx context.Context, hostname string, register bool, log logr.Logger, c client.Client) error { if register { return resource.RegisterNodeExtendedResource(ctx, hostname, resource.UIRequestResource, successResourceValue, c) } // remove node resource and deschedule pods if we want to deregister descheduledPods, err := resource.DeregisterNodeExtendedResourceAndDeschedulePods(ctx, hostname, resource.UIRequestResource, c, client.HasLabels{resource.UIRequestResource.String()}) if err != nil { return err } log.Info("removed node resource and descheduled any UI pods", "resource", resource.UIRequestResource, "pods", descheduledPods) return nil } func (c *NodeDisplayConfigController) summarizeAndPatch(ctx context.Context, nodeDisplayConfig *v2.NodeDisplayConfig, result reconcile.Result, err error, patcher *patch.SerialPatcher) (ctrl.Result, error) { s := reconcile.NewSummarizer(patcher) return s.SummarizeAndPatch( ctx, nodeDisplayConfig, reconcile.WithResult(result), reconcile.WithError(err), reconcile.WithIgnoreNotFound(), reconcile.WithFieldOwner(c.Name), reconcile.WithConditions(reconcile.Conditions{ Target: status.ReadyCondition, Owned: []string{v2.DisplayManagerConfiguredCondition}, Summarize: []string{v2.DisplayManagerConfiguredCondition}, }), reconcile.WithProcessors( reconcile.RecordReconcileReq, reconcile.RecordResult, ), ) } func (c *NodeDisplayConfigController) applyNodeDisplayConfig(ctx context.Context, nodeDisplayConfig *v2.NodeDisplayConfig) (*v2.DisplayConfig, error) { appliedDisplayConfig, err := displayconfig.Apply(ctx, nodeDisplayConfig.Spec, c.DisplayManager) if err != nil { return nil, fmt.Errorf("failed to apply NodeDisplayConfig: %w", err) } return appliedDisplayConfig, nil } func getDisplayctlEnabled(ctx context.Context, hostname string, c client.Client) (enabled bool, configName string, err error) { config, err := xserverconfig.FromClient(ctx, hostname, c) if kerrors.IsNotFound(err) { return true, "", nil } else if err != nil { return false, "", err } return config.DisplayctlEnabled(), config.ConfigMapName(), nil } // Log warnings when configuration may not be applied as expected. func logConfigurationWarnings(configuredDisplayConfig, appliedDisplayConfig *v2.DisplayConfig, log logr.Logger) { if configuredDisplayConfig != nil { logDisplayConfigurationWarnings(configuredDisplayConfig.Displays, appliedDisplayConfig.Displays, log) } logDuplicateInputDeviceMappingWarnings(appliedDisplayConfig.Displays, log) } // Log warning when a display was configured but not applied. func logDisplayConfigurationWarnings(configuredDisplays, appliedDisplays v2.Displays, log logr.Logger) { dps := []v2.DisplayPort{} for _, configuredDisplay := range configuredDisplays { if appliedDisplay := appliedDisplays.FindByDisplayPort(configuredDisplay.DisplayPort); appliedDisplay == nil { dps = append(dps, configuredDisplay.DisplayPort) } } if len(dps) > 0 { log.Info( "WARNING: display(s) specified in host NodeDisplayConfig are not connected and will be ignored", "displays", dps, ) } } // Log warning when multiple input device mappings reference the same input device name. func logDuplicateInputDeviceMappingWarnings(appliedDisplays v2.Displays, log logr.Logger) { inputDeviceNameMappings := map[v2.InputDeviceName][]v2.DisplayPort{} for _, display := range appliedDisplays { for _, inputDeviceMapping := range display.InputDeviceMappings { inputDeviceNameMappings[inputDeviceMapping] = append(inputDeviceNameMappings[inputDeviceMapping], display.DisplayPort) } } for inputDeviceName, dps := range inputDeviceNameMappings { if len(dps) > 1 { log.Info( "WARNING: multiple mappings specify the same input device name - input devices may be mapped to the wrong displays", "input device name", inputDeviceName, "mapped displays", dps, ) } } }