Files
kubernetes/pkg/volume/csi/csi_node_updater.go
Eddie Torres c766a52356 Implement KEP 4876 Mutable CSINode (#130007)
* Implement KEP-4876 Mutable CSINode Allocatable Count

Signed-off-by: torredil <torredil@amazon.com>

* Update TestGetNodeAllocatableUpdatePeriod

Signed-off-by: torredil <torredil@amazon.com>

* Implement CSINodeUpdater

Signed-off-by: torredil <torredil@amazon.com>

* Use sync.Once in csiNodeUpdater

Signed-off-by: torredil <torredil@amazon.com>

* ImVerify driver is installed before running periodic updates

Signed-off-by: torredil <torredil@amazon.com>

* Update NodeAllocatableUpdatePeriodSeconds type comment

Signed-off-by: torredil <torredil@amazon.com>

* Leverage apivalidation.ValidateImmutableField in ValidateCSINodeUpdate

Signed-off-by: torredil <torredil@amazon.com>

* Update strategy functions

Signed-off-by: torredil <torredil@amazon.com>

* Run hack/update-openapi-spec.sh

Signed-off-by: torredil <torredil@amazon.com>

* Update VolumeError.ErrorCode field

Signed-off-by: torredil <torredil@amazon.com>

* CSINodeUpdater improvements

Signed-off-by: torredil <torredil@amazon.com>

* Iron out concurrency in syncDriverUpdater

Signed-off-by: torredil <torredil@amazon.com>

* Run hack/update-openapi-spec.sh

Signed-off-by: torredil <torredil@amazon.com>

* Revise logging

Signed-off-by: torredil <torredil@amazon.com>

* Revise log in VerifyExhaustedResource

Signed-off-by: torredil <torredil@amazon.com>

* Update API validation

Signed-off-by: torredil <torredil@amazon.com>

* Add more code coverage

Signed-off-by: torredil <torredil@amazon.com>

* Fix pull-kubernetes-linter-hints

Signed-off-by: torredil <torredil@amazon.com>

* Update API types documentation

Signed-off-by: torredil <torredil@amazon.com>

* Update strategy and validation for new errorCode field

Signed-off-by: torredil <torredil@amazon.com>

* Update validation tests after strategy changes

Signed-off-by: torredil <torredil@amazon.com>

* Update VA status strategy

Signed-off-by: torredil <torredil@amazon.com>

---------

Signed-off-by: torredil <torredil@amazon.com>
2025-03-18 12:45:49 -07:00

200 lines
6.3 KiB
Go

/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package csi
import (
"fmt"
"sync"
"time"
v1 "k8s.io/api/storage/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/klog/v2"
)
// csiNodeUpdater watches for changes to CSIDriver objects and manages the lifecycle
// of per-driver goroutines that periodically update CSINodeDriver.Allocatable information
// based on the NodeAllocatableUpdatePeriodSeconds setting.
type csiNodeUpdater struct {
// Informer for CSIDriver objects
driverInformer cache.SharedIndexInformer
// Map of driver names to stop channels for update goroutines
driverUpdaters sync.Map
// Ensures the updater is only started once
once sync.Once
}
// NewCSINodeUpdater creates a new csiNodeUpdater
func NewCSINodeUpdater(driverInformer cache.SharedIndexInformer) (*csiNodeUpdater, error) {
if driverInformer == nil {
return nil, fmt.Errorf("driverInformer must not be nil")
}
return &csiNodeUpdater{
driverInformer: driverInformer,
driverUpdaters: sync.Map{},
}, nil
}
// Run starts the csiNodeUpdater by registering event handlers.
func (u *csiNodeUpdater) Run() {
u.once.Do(func() {
_, err := u.driverInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: u.onDriverAdd,
UpdateFunc: u.onDriverUpdate,
DeleteFunc: u.onDriverDelete,
})
if err != nil {
klog.ErrorS(err, "Failed to add event handler for CSI driver informer")
return
}
klog.V(4).InfoS("csiNodeUpdater initialized successfully")
})
}
// onDriverAdd handles the addition of a new CSIDriver object.
func (u *csiNodeUpdater) onDriverAdd(obj interface{}) {
driver, ok := obj.(*v1.CSIDriver)
if !ok {
return
}
klog.V(7).InfoS("onDriverAdd event", "driver", driver.Name)
u.syncDriverUpdater(driver.Name)
}
// onDriverUpdate handles updates to CSIDriver objects.
func (u *csiNodeUpdater) onDriverUpdate(oldObj, newObj interface{}) {
oldDriver, ok := oldObj.(*v1.CSIDriver)
if !ok {
return
}
newDriver, ok := newObj.(*v1.CSIDriver)
if !ok {
return
}
// Only reconfigure if the NodeAllocatableUpdatePeriodSeconds field is updated.
oldPeriod := getNodeAllocatableUpdatePeriod(oldDriver)
newPeriod := getNodeAllocatableUpdatePeriod(newDriver)
if oldPeriod != newPeriod {
klog.V(4).InfoS("NodeAllocatableUpdatePeriodSeconds updated", "driver", newDriver.Name, "oldPeriod", oldPeriod, "newPeriod", newPeriod)
u.syncDriverUpdater(newDriver.Name)
}
}
// onDriverDelete handles deletion of CSIDriver objects.
func (u *csiNodeUpdater) onDriverDelete(obj interface{}) {
driver, ok := obj.(*v1.CSIDriver)
if !ok {
return
}
klog.V(7).InfoS("onDriverDelete event", "driver", driver.Name)
u.syncDriverUpdater(driver.Name)
}
// syncDriverUpdater re-evaluates whether the periodic updater for a given driver should run.
// It is invoked from informer events (Add/Update/Delete) and from plugin registration/deregistration.
func (u *csiNodeUpdater) syncDriverUpdater(driverName string) {
// Check if the CSI plugin is installed on this node.
if !isDriverInstalled(driverName) {
klog.V(4).InfoS("Driver not installed; stopping csiNodeUpdater", "driver", driverName)
u.unregisterDriver(driverName)
return
}
// Get the CSIDriver object from the informer cache.
obj, exists, err := u.driverInformer.GetStore().GetByKey(driverName)
if err != nil {
u.unregisterDriver(driverName)
klog.ErrorS(err, "Error retrieving CSIDriver from store", "driver", driverName)
return
}
if !exists {
klog.InfoS("CSIDriver object not found; stopping csiNodeUpdater", "driver", driverName)
u.unregisterDriver(driverName)
return
}
driver, ok := obj.(*v1.CSIDriver)
if !ok {
klog.ErrorS(fmt.Errorf("invalid CSIDriver object type"), "failed to cast CSIDriver object", "driver", driverName)
return
}
// Get the update period.
period := getNodeAllocatableUpdatePeriod(driver)
if period == 0 {
klog.V(7).InfoS("NodeAllocatableUpdatePeriodSeconds is not configured; disabling updates", "driver", driverName)
u.unregisterDriver(driverName)
return
}
newStopCh := make(chan struct{})
prevStopCh, loaded := u.driverUpdaters.Swap(driverName, newStopCh)
// If an updater is already running, stop it so we can reconfigure.
if loaded && prevStopCh != nil {
if stopCh, ok := prevStopCh.(chan struct{}); ok {
close(stopCh)
}
}
// Start the periodic update goroutine.
go u.runPeriodicUpdate(driverName, period, newStopCh)
}
// unregisterDriver stops any running periodic update goroutine for the given driver.
func (u *csiNodeUpdater) unregisterDriver(driverName string) {
prev, loaded := u.driverUpdaters.LoadAndDelete(driverName)
if loaded && prev != nil {
if stopCh, ok := prev.(chan struct{}); ok {
close(stopCh)
}
}
}
// runPeriodicUpdate runs the periodic update loop for a driver.
func (u *csiNodeUpdater) runPeriodicUpdate(driverName string, period time.Duration, stopCh <-chan struct{}) {
ticker := time.NewTicker(period)
defer ticker.Stop()
klog.V(7).InfoS("Starting periodic updates for driver", "driver", driverName, "period", period)
for {
select {
case <-ticker.C:
if err := updateCSIDriver(driverName); err != nil {
klog.ErrorS(err, "Failed to update CSIDriver", "driver", driverName)
}
case <-stopCh:
klog.V(4).InfoS("Stopping periodic updates for driver", "driver", driverName, "period", period)
return
}
}
}
// isDriverInstalled checks if the CSI driver is installed on the node by checking the global csiDrivers map
func isDriverInstalled(driverName string) bool {
_, ok := csiDrivers.Get(driverName)
return ok
}
// getNodeAllocatableUpdatePeriod returns the NodeAllocatableUpdatePeriodSeconds value from the CSIDriver
func getNodeAllocatableUpdatePeriod(driver *v1.CSIDriver) time.Duration {
if driver == nil || driver.Spec.NodeAllocatableUpdatePeriodSeconds == nil {
return 0
}
return time.Duration(*driver.Spec.NodeAllocatableUpdatePeriodSeconds) * time.Second
}