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
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
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
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
141
142
143
144
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
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
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
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
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
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
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
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
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
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
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
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
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
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