Skip to content

Commit e8b7bce

Browse files
committed
Add KEP-4680 device health reporting with message field support
1 parent 70f9b54 commit e8b7bce

7 files changed

Lines changed: 846 additions & 3 deletions

File tree

cmd/dra-example-kubeletplugin/driver.go

Lines changed: 294 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,27 +20,58 @@ import (
2020
"context"
2121
"errors"
2222
"fmt"
23+
"os"
24+
"strings"
25+
"sync"
26+
"time"
2327

28+
"google.golang.org/grpc"
29+
corev1 "k8s.io/api/core/v1"
2430
resourceapi "k8s.io/api/resource/v1"
31+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2532
"k8s.io/apimachinery/pkg/types"
2633
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
2734
coreclientset "k8s.io/client-go/kubernetes"
2835
"k8s.io/dynamic-resource-allocation/kubeletplugin"
2936
"k8s.io/klog/v2"
37+
drahealthv1alpha1 "k8s.io/kubelet/pkg/apis/dra-health/v1alpha1"
3038
)
3139

40+
type DeviceHealthStatus struct {
41+
Health drahealthv1alpha1.HealthStatus
42+
Message string
43+
}
44+
3245
type driver struct {
46+
drahealthv1alpha1.UnimplementedDRAResourceHealthServer
47+
3348
client coreclientset.Interface
3449
helper *kubeletplugin.Helper
3550
state *DeviceState
3651
healthcheck *healthcheck
3752
cancelCtx func(error)
53+
54+
config *Config
55+
poolName string
56+
simulator *HealthSimulator
57+
healthMu sync.RWMutex
58+
deviceHealth map[string]*DeviceHealthStatus
59+
healthClients []chan *drahealthv1alpha1.NodeWatchResourcesResponse
60+
clientsMu sync.RWMutex
61+
healthOverrides map[string]bool
62+
stopHealthCh chan struct{}
63+
healthWg sync.WaitGroup
3864
}
3965

4066
func NewDriver(ctx context.Context, config *Config) (*driver, error) {
4167
driver := &driver{
42-
client: config.coreclient,
43-
cancelCtx: config.cancelMainCtx,
68+
client: config.coreclient,
69+
cancelCtx: config.cancelMainCtx,
70+
config: config,
71+
poolName: config.flags.nodeName,
72+
deviceHealth: make(map[string]*DeviceHealthStatus),
73+
healthOverrides: make(map[string]bool),
74+
stopHealthCh: make(chan struct{}),
4475
}
4576

4677
state, err := NewDeviceState(config)
@@ -49,6 +80,18 @@ func NewDriver(ctx context.Context, config *Config) (*driver, error) {
4980
}
5081
driver.state = state
5182

83+
var deviceNames []string
84+
for deviceName := range state.allocatable {
85+
deviceNames = append(deviceNames, deviceName)
86+
driver.deviceHealth[deviceName] = &DeviceHealthStatus{
87+
Health: drahealthv1alpha1.HealthStatus_HEALTHY,
88+
Message: fmt.Sprintf("Device %s initialized successfully", deviceName),
89+
}
90+
}
91+
92+
driver.simulator = NewHealthSimulator(deviceNames)
93+
klog.Infof("Device health reporting enabled for %d devices", len(deviceNames))
94+
5295
helper, err := kubeletplugin.Start(ctx, driver,
5396
kubeletplugin.KubeClient(config.coreclient),
5497
kubeletplugin.NodeName(config.flags.nodeName),
@@ -71,13 +114,30 @@ func NewDriver(ctx context.Context, config *Config) (*driver, error) {
71114
return nil, err
72115
}
73116

117+
driver.healthWg.Add(2)
118+
go driver.healthMonitoringLoop(ctx)
119+
go driver.watchHealthOverrides(ctx)
120+
74121
return driver, nil
75122
}
76123

77124
func (d *driver) Shutdown(logger klog.Logger) error {
78125
if d.healthcheck != nil {
79126
d.healthcheck.Stop(logger)
80127
}
128+
129+
logger.Info("Stopping device health monitoring")
130+
close(d.stopHealthCh)
131+
132+
d.clientsMu.Lock()
133+
for _, clientCh := range d.healthClients {
134+
close(clientCh)
135+
}
136+
d.healthClients = nil
137+
d.clientsMu.Unlock()
138+
139+
d.healthWg.Wait()
140+
81141
d.helper.Stop()
82142
return nil
83143
}
@@ -144,3 +204,235 @@ func (d *driver) HandleError(ctx context.Context, err error, msg string) {
144204
d.cancelCtx(fmt.Errorf("fatal background error: %w", err))
145205
}
146206
}
207+
208+
func (d *driver) healthMonitoringLoop(ctx context.Context) {
209+
defer d.healthWg.Done()
210+
211+
logger := klog.FromContext(ctx)
212+
ticker := time.NewTicker(30 * time.Second)
213+
defer ticker.Stop()
214+
215+
logger.Info("Starting device health monitoring loop")
216+
d.performHealthCheck(logger)
217+
218+
for {
219+
select {
220+
case <-d.stopHealthCh:
221+
logger.Info("Health monitoring loop stopped")
222+
return
223+
case <-ctx.Done():
224+
return
225+
case <-ticker.C:
226+
d.performHealthCheck(logger)
227+
}
228+
}
229+
}
230+
231+
const healthAnnotationPrefix = "health.example.com/"
232+
233+
func (d *driver) watchHealthOverrides(ctx context.Context) {
234+
defer d.healthWg.Done()
235+
236+
logger := klog.FromContext(ctx)
237+
podName, _ := os.Hostname()
238+
namespace := os.Getenv("NAMESPACE")
239+
if podName == "" || namespace == "" {
240+
logger.Info("Pod identity not available, health overrides disabled")
241+
return
242+
}
243+
244+
for {
245+
watcher, err := d.client.CoreV1().Pods(namespace).Watch(ctx, metav1.ListOptions{
246+
FieldSelector: "metadata.name=" + podName,
247+
})
248+
if err != nil {
249+
logger.Error(err, "Failed to watch pod for health overrides")
250+
select {
251+
case <-ctx.Done():
252+
return
253+
case <-d.stopHealthCh:
254+
return
255+
case <-time.After(5 * time.Second):
256+
continue
257+
}
258+
}
259+
260+
for event := range watcher.ResultChan() {
261+
select {
262+
case <-ctx.Done():
263+
watcher.Stop()
264+
return
265+
case <-d.stopHealthCh:
266+
watcher.Stop()
267+
return
268+
default:
269+
}
270+
271+
pod, ok := event.Object.(*corev1.Pod)
272+
if !ok {
273+
continue
274+
}
275+
d.applyHealthOverrides(logger, pod.Annotations)
276+
}
277+
}
278+
}
279+
280+
func (d *driver) applyHealthOverrides(logger klog.Logger, annotations map[string]string) {
281+
d.healthMu.Lock()
282+
283+
activeOverrides := make(map[string]bool)
284+
for key, value := range annotations {
285+
if !strings.HasPrefix(key, healthAnnotationPrefix) {
286+
continue
287+
}
288+
deviceName := strings.TrimPrefix(key, healthAnnotationPrefix)
289+
if _, exists := d.deviceHealth[deviceName]; !exists {
290+
continue
291+
}
292+
293+
activeOverrides[deviceName] = true
294+
if d.healthOverrides[deviceName] {
295+
continue
296+
}
297+
298+
var scenario HealthScenario
299+
switch strings.ToLower(value) {
300+
case "unhealthy":
301+
scenario = ScenarioTemperatureWarning
302+
case "unknown":
303+
scenario = ScenarioCommunicationFailure
304+
default:
305+
scenario = ScenarioHealthy
306+
}
307+
308+
d.simulator.ForceScenario(deviceName, scenario)
309+
d.healthOverrides[deviceName] = true
310+
logger.Info("Health override applied", "device", deviceName, "status", value)
311+
}
312+
313+
for deviceName := range d.healthOverrides {
314+
if !activeOverrides[deviceName] {
315+
d.simulator.ForceScenario(deviceName, ScenarioHealthy)
316+
delete(d.healthOverrides, deviceName)
317+
logger.Info("Health override removed, returning to simulation", "device", deviceName)
318+
}
319+
}
320+
321+
d.healthMu.Unlock()
322+
323+
d.performHealthCheck(logger)
324+
}
325+
326+
func (d *driver) performHealthCheck(logger klog.Logger) {
327+
d.healthMu.Lock()
328+
329+
hasChanges := false
330+
for deviceName, currentHealth := range d.deviceHealth {
331+
newHealth, newMessage := d.simulator.GetDeviceHealth(deviceName)
332+
if currentHealth.Health != newHealth || currentHealth.Message != newMessage {
333+
currentHealth.Health = newHealth
334+
currentHealth.Message = newMessage
335+
hasChanges = true
336+
logger.Info("Device health changed",
337+
"device", deviceName,
338+
"health", newHealth.String(),
339+
"message", newMessage)
340+
}
341+
}
342+
343+
d.healthMu.Unlock()
344+
345+
if hasChanges {
346+
d.notifyClients()
347+
}
348+
}
349+
350+
func (d *driver) buildHealthResponse() *drahealthv1alpha1.NodeWatchResourcesResponse {
351+
var devices []*drahealthv1alpha1.DeviceHealth
352+
for deviceName, health := range d.deviceHealth {
353+
devices = append(devices, &drahealthv1alpha1.DeviceHealth{
354+
Device: &drahealthv1alpha1.DeviceIdentifier{
355+
PoolName: d.poolName,
356+
DeviceName: deviceName,
357+
},
358+
Health: health.Health,
359+
LastUpdatedTime: time.Now().Unix(),
360+
Message: health.Message,
361+
HealthCheckTimeoutSeconds: 60,
362+
})
363+
}
364+
365+
return &drahealthv1alpha1.NodeWatchResourcesResponse{
366+
Devices: devices,
367+
}
368+
}
369+
370+
func (d *driver) notifyClients() {
371+
d.healthMu.RLock()
372+
response := d.buildHealthResponse()
373+
d.healthMu.RUnlock()
374+
375+
d.clientsMu.RLock()
376+
defer d.clientsMu.RUnlock()
377+
378+
for _, clientCh := range d.healthClients {
379+
select {
380+
case clientCh <- response:
381+
default:
382+
}
383+
}
384+
}
385+
386+
func (d *driver) NodeWatchResources(
387+
req *drahealthv1alpha1.NodeWatchResourcesRequest,
388+
stream grpc.ServerStreamingServer[drahealthv1alpha1.NodeWatchResourcesResponse],
389+
) error {
390+
logger := klog.FromContext(stream.Context())
391+
logger.Info("New health monitoring client connected")
392+
393+
// Create a channel for this client
394+
clientCh := make(chan *drahealthv1alpha1.NodeWatchResourcesResponse, 10)
395+
396+
// Register the client
397+
d.clientsMu.Lock()
398+
d.healthClients = append(d.healthClients, clientCh)
399+
d.clientsMu.Unlock()
400+
401+
// Cleanup on exit
402+
defer func() {
403+
d.clientsMu.Lock()
404+
for i, ch := range d.healthClients {
405+
if ch == clientCh {
406+
d.healthClients = append(d.healthClients[:i], d.healthClients[i+1:]...)
407+
break
408+
}
409+
}
410+
d.clientsMu.Unlock()
411+
close(clientCh)
412+
logger.Info("Health monitoring client disconnected")
413+
}()
414+
415+
d.healthMu.RLock()
416+
initialResponse := d.buildHealthResponse()
417+
d.healthMu.RUnlock()
418+
419+
if err := stream.Send(initialResponse); err != nil {
420+
return fmt.Errorf("failed to send initial health status: %w", err)
421+
}
422+
423+
// Stream updates
424+
for {
425+
select {
426+
case <-stream.Context().Done():
427+
return stream.Context().Err()
428+
case response, ok := <-clientCh:
429+
if !ok {
430+
// Channel closed, exit
431+
return nil
432+
}
433+
if err := stream.Send(response); err != nil {
434+
return fmt.Errorf("failed to send health update: %w", err)
435+
}
436+
}
437+
}
438+
}

0 commit comments

Comments
 (0)