@@ -21,12 +21,14 @@ import (
21
21
"errors"
22
22
"fmt"
23
23
"net"
24
+ "os"
24
25
"path"
25
26
"sync"
26
27
27
28
"google.golang.org/grpc"
28
29
"k8s.io/klog/v2"
29
30
31
+ "go.etcd.io/etcd/client/pkg/v3/fileutil"
30
32
resourceapi "k8s.io/api/resource/v1beta1"
31
33
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32
34
"k8s.io/apimachinery/pkg/types"
@@ -195,8 +197,10 @@ func RegistrarDirectoryPath(path string) Option {
195
197
// support updates from an installation which used an older release of
196
198
// of the helper code.
197
199
//
198
- // The default is <driver name>-reg.sock. When rolling updates are enabled (not supported yet) ,
200
+ // The default is <driver name>-reg.sock. When rolling updates are enabled,
199
201
// it is <driver name>-<uid>-reg.sock.
202
+ //
203
+ // This option and [RollingUpdate] are mutually exclusive.
200
204
func RegistrarSocketFilename (name string ) Option {
201
205
return func (o * options ) error {
202
206
o .pluginRegistrationEndpoint .file = name
@@ -248,6 +252,44 @@ func PluginListener(listen func(ctx context.Context, path string) (net.Listener,
248
252
}
249
253
}
250
254
255
+ // RollingUpdate can be used to enable support for running two plugin instances
256
+ // in parallel while a newer instance replaces the older. When enabled, both
257
+ // instances must share the same plugin data directory and driver name.
258
+ // They create different sockets to allow the kubelet to connect to both at
259
+ // the same time.
260
+ //
261
+ // There is no guarantee which of the two instances are used by kubelet.
262
+ // For example, it can happen that a claim gets prepared by one instance
263
+ // and then needs to be unprepared by the other. Kubelet then may fall back
264
+ // to the first one again for some other operation. In practice this means
265
+ // that each instance must be entirely stateless across method calls.
266
+ // Serialization (on by default, see [Serialize]) ensures that methods
267
+ // are serialized across all instances through file locking. The plugin
268
+ // implementation can load shared state from a file at the start
269
+ // of a call, execute and then store the updated shared state again.
270
+ //
271
+ // Passing a non-empty uid enables rolling updates, an empty uid disables it.
272
+ // The uid must be the pod UID. A DaemonSet can pass that into the driver container
273
+ // via the downward API (https://kubernetes.io/docs/concepts/workloads/pods/downward-api/#downwardapi-fieldRef).
274
+ //
275
+ // Because new instances cannot remove stale sockets of older instances,
276
+ // it is important that each pod shuts down cleanly: it must catch SIGINT/TERM
277
+ // and stop the helper instead of quitting immediately.
278
+ //
279
+ // This depends on support in the kubelet which was added in Kubernetes 1.33.
280
+ // Don't use this if it is not certain that the kubelet has that support!
281
+ //
282
+ // This option and [RegistrarSocketFilename] are mutually exclusive.
283
+ func RollingUpdate (uid types.UID ) Option {
284
+ return func (o * options ) error {
285
+ o .rollingUpdateUID = uid
286
+
287
+ // TODO: ask the kubelet whether that pod is still running and
288
+ // clean up leftover sockets?
289
+ return nil
290
+ }
291
+ }
292
+
251
293
// GRPCInterceptor is called for each incoming gRPC method call. This option
252
294
// may be used more than once and each interceptor will get called.
253
295
func GRPCInterceptor (interceptor grpc.UnaryServerInterceptor ) Option {
@@ -322,6 +364,17 @@ func Serialize(enabled bool) Option {
322
364
}
323
365
}
324
366
367
+ // FlockDir changes where lock files are created and locked. A lock file
368
+ // is needed when serializing gRPC calls and rolling updates are enabled.
369
+ // The directory must exist and be reserved for exclusive use by the
370
+ // driver. The default is the plugin data directory.
371
+ func FlockDirectoryPath (path string ) Option {
372
+ return func (o * options ) error {
373
+ o .flockDirectoryPath = path
374
+ return nil
375
+ }
376
+ }
377
+
325
378
type options struct {
326
379
logger klog.Logger
327
380
grpcVerbosity int
@@ -330,11 +383,13 @@ type options struct {
330
383
nodeUID types.UID
331
384
pluginRegistrationEndpoint endpoint
332
385
pluginDataDirectoryPath string
386
+ rollingUpdateUID types.UID
333
387
draEndpointListen func (ctx context.Context , path string ) (net.Listener , error )
334
388
unaryInterceptors []grpc.UnaryServerInterceptor
335
389
streamInterceptors []grpc.StreamServerInterceptor
336
390
kubeClient kubernetes.Interface
337
391
serialize bool
392
+ flockDirectoryPath string
338
393
nodeV1beta1 bool
339
394
}
340
395
@@ -344,17 +399,18 @@ type Helper struct {
344
399
// backgroundCtx is for activities that are started later.
345
400
backgroundCtx context.Context
346
401
// cancel cancels the backgroundCtx.
347
- cancel func (cause error )
348
- wg sync.WaitGroup
349
- registrar * nodeRegistrar
350
- pluginServer * grpcServer
351
- plugin DRAPlugin
352
- driverName string
353
- nodeName string
354
- nodeUID types.UID
355
- kubeClient kubernetes.Interface
356
- serialize bool
357
- grpcMutex sync.Mutex
402
+ cancel func (cause error )
403
+ wg sync.WaitGroup
404
+ registrar * nodeRegistrar
405
+ pluginServer * grpcServer
406
+ plugin DRAPlugin
407
+ driverName string
408
+ nodeName string
409
+ nodeUID types.UID
410
+ kubeClient kubernetes.Interface
411
+ serialize bool
412
+ grpcMutex sync.Mutex
413
+ grpcLockFilePath string
358
414
359
415
// Information about resource publishing changes concurrently and thus
360
416
// must be protected by the mutex. The controller gets started only
@@ -392,12 +448,20 @@ func Start(ctx context.Context, plugin DRAPlugin, opts ...Option) (result *Helpe
392
448
if o .driverName == "" {
393
449
return nil , errors .New ("driver name must be set" )
394
450
}
451
+ if o .rollingUpdateUID != "" && o .pluginRegistrationEndpoint .file != "" {
452
+ return nil , errors .New ("rolling updates and explicit registration socket filename are mutually exclusive" )
453
+ }
454
+ uidPart := ""
455
+ if o .rollingUpdateUID != "" {
456
+ uidPart = "-" + string (o .rollingUpdateUID )
457
+ }
395
458
if o .pluginRegistrationEndpoint .file == "" {
396
- o .pluginRegistrationEndpoint .file = o .driverName + "-reg.sock"
459
+ o .pluginRegistrationEndpoint .file = o .driverName + uidPart + "-reg.sock"
397
460
}
398
461
if o .pluginDataDirectoryPath == "" {
399
462
o .pluginDataDirectoryPath = path .Join (KubeletPluginsDir , o .driverName )
400
463
}
464
+
401
465
d := & Helper {
402
466
driverName : o .driverName ,
403
467
nodeName : o .nodeName ,
@@ -406,6 +470,14 @@ func Start(ctx context.Context, plugin DRAPlugin, opts ...Option) (result *Helpe
406
470
serialize : o .serialize ,
407
471
plugin : plugin ,
408
472
}
473
+ if o .rollingUpdateUID != "" {
474
+ dir := o .pluginDataDirectoryPath
475
+ if o .flockDirectoryPath != "" {
476
+ dir = o .flockDirectoryPath
477
+ }
478
+ // Enable file locking, required for concurrently running pods.
479
+ d .grpcLockFilePath = path .Join (dir , "serialize.lock" )
480
+ }
409
481
410
482
// Stop calls cancel and therefore both cancellation
411
483
// and Stop cause goroutines to stop.
@@ -434,7 +506,7 @@ func Start(ctx context.Context, plugin DRAPlugin, opts ...Option) (result *Helpe
434
506
var supportedServices []string
435
507
draEndpoint := endpoint {
436
508
dir : o .pluginDataDirectoryPath ,
437
- file : "dra.sock" , // "dra" is hard-coded.
509
+ file : "dra" + uidPart + " .sock" , // "dra" is hard-coded. The directory is unique, so we get a unique full path also without the UID .
438
510
listenFunc : o .draEndpointListen ,
439
511
}
440
512
pluginServer , err := startGRPCServer (klog .LoggerWithName (logger , "dra" ), o .grpcVerbosity , o .unaryInterceptors , o .streamInterceptors , draEndpoint , func (grpcServer * grpc.Server ) {
@@ -575,12 +647,25 @@ func (d *Helper) RegistrationStatus() *registerapi.RegistrationStatus {
575
647
// serializeGRPCIfEnabled locks a mutex if serialization is enabled.
576
648
// Either way it returns a method that the caller must invoke
577
649
// via defer.
578
- func (d * Helper ) serializeGRPCIfEnabled () func () {
650
+ func (d * Helper ) serializeGRPCIfEnabled () ( func (), error ) {
579
651
if ! d .serialize {
580
- return func () {}
652
+ return func () {}, nil
581
653
}
654
+
655
+ // If rolling updates are enabled, we cannot do only in-memory locking.
656
+ // We must use file locking.
657
+ if d .grpcLockFilePath != "" {
658
+ file , err := fileutil .LockFile (d .grpcLockFilePath , os .O_RDWR | os .O_CREATE , 0666 )
659
+ if err != nil {
660
+ return nil , fmt .Errorf ("lock file: %w" , err )
661
+ }
662
+ return func () {
663
+ _ = file .Close ()
664
+ }, nil
665
+ }
666
+
582
667
d .grpcMutex .Lock ()
583
- return d .grpcMutex .Unlock
668
+ return d .grpcMutex .Unlock , nil
584
669
}
585
670
586
671
// nodePluginImplementation is a thin wrapper around the helper instance.
@@ -597,7 +682,11 @@ func (d *nodePluginImplementation) NodePrepareResources(ctx context.Context, req
597
682
return nil , fmt .Errorf ("get resource claims: %w" , err )
598
683
}
599
684
600
- defer d .serializeGRPCIfEnabled ()()
685
+ unlock , err := d .serializeGRPCIfEnabled ()
686
+ if err != nil {
687
+ return nil , fmt .Errorf ("serialize gRPC: %w" , err )
688
+ }
689
+ defer unlock ()
601
690
602
691
result , err := d .plugin .PrepareResourceClaims (ctx , claims )
603
692
if err != nil {
@@ -659,7 +748,11 @@ func (d *nodePluginImplementation) getResourceClaims(ctx context.Context, claims
659
748
660
749
// NodeUnprepareResources implements [draapi.NodeUnprepareResources].
661
750
func (d * nodePluginImplementation ) NodeUnprepareResources (ctx context.Context , req * drapb.NodeUnprepareResourcesRequest ) (* drapb.NodeUnprepareResourcesResponse , error ) {
662
- defer d .serializeGRPCIfEnabled ()
751
+ unlock , err := d .serializeGRPCIfEnabled ()
752
+ if err != nil {
753
+ return nil , fmt .Errorf ("serialize gRPC: %w" , err )
754
+ }
755
+ defer unlock ()
663
756
664
757
claims := make ([]NamespacedObject , 0 , len (req .Claims ))
665
758
for _ , claim := range req .Claims {
0 commit comments