Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 70 additions & 15 deletions cmd/driver-manager/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,13 @@ const (
nvidiaDRAValidatorDeployLabel = nvidiaDomainPrefix + "/" + "gpu.deploy.dra-validator"
nvidiaDRADCGMExporterDeployLabel = nvidiaDomainPrefix + "/" + "gpu.deploy.dcgm-exporter-dra"
nvidiaDRADCGMDeployLabel = nvidiaDomainPrefix + "/" + "gpu.deploy.dcgm-dra"

// nvidiaDriverManagerCordonAnnotation marks a cordon applied by driver-manager, since
// Node.Spec.Unschedulable does not record who set it.
nvidiaDriverManagerCordonAnnotation = nvidiaDomainPrefix + "/" + "gpu-driver-manager.node-cordoned"
// nvidiaUpgradeControllerCordonAnnotation is the ownership marker proposed for the upgrade
// controller, which does not write it yet.
nvidiaUpgradeControllerCordonAnnotation = nvidiaDomainPrefix + "/" + "gpu-driver-upgrade-controller.node-cordoned"
)

// Configuration holds all the configuration from environment variables
Expand Down Expand Up @@ -293,6 +300,8 @@ func newDriverManager(ctx context.Context, cfg *config, components *componentSta
func (dm *DriverManager) uninstallDriver() error {
dm.log.Info("Starting driver uninstallation process")

dm.clearStaleCordonClaim()

// Check if driver is pre-installed on host
if dm.isHostDriver() {
dm.log.Info("NVIDIA GPU driver is already pre-installed on the node, disabling the containerized driver")
Expand Down Expand Up @@ -369,6 +378,12 @@ func (dm *DriverManager) uninstallDriver() error {
return nil
}

// Wait out an external cordon before evicting anything, so the node keeps running
// untouched while it is in place.
if err := dm.waitForExternalCordonToClear(); err != nil {
return err
}

// Always evict all GPU operator components across a driver restart. The DRA
// kubelet-plugin is the exception: it services NodeUnprepareResources for the
// claim-holders evicted here (e.g. dra-validator), so it must outlive them and
Expand Down Expand Up @@ -1225,6 +1240,61 @@ func (dm *DriverManager) isGPUPodEvictionEnabled() bool {
return dm.config.enableGPUPodEviction
}

// waitForExternalCordonToClear blocks while the node is cordoned by someone other than
// GPU Operator. A GPU-owned cordon is in-progress operator work and does not block.
func (dm *DriverManager) waitForExternalCordonToClear() error {
err := dm.kubeClient.WaitUntilNoExternalCordon(dm.config.nodeName,
nvidiaDriverManagerCordonAnnotation, nvidiaUpgradeControllerCordonAnnotation)
if err != nil {
return fmt.Errorf("failed while waiting for an external cordon on node %s to clear: %w",
dm.config.nodeName, err)
}
return nil
}

// clearStaleCordonClaim is best effort and must not block the driver install.
func (dm *DriverManager) clearStaleCordonClaim() {
if err := dm.kubeClient.ClearStaleCordonClaim(dm.config.nodeName, nvidiaDriverManagerCordonAnnotation); err != nil {
dm.log.Warnf("Failed to clear stale cordon claim on node %s: %v", dm.config.nodeName, err)
}
}

// cordonNode cordons the node and records driver-manager as its owner. An existing external
// cordon is waited out rather than taken over.
func (dm *DriverManager) cordonNode() error {
for {
if err := dm.waitForExternalCordonToClear(); err != nil {
return err
}
var acquired bool
err := retryOnAnyError(dm.ctx, dm.log, retryBackoff(defaultCordonRetries),
fmt.Sprintf("cordon node %s", dm.config.nodeName), func() error {
var err error
acquired, err = dm.kubeClient.AcquireNodeCordon(dm.config.nodeName,
nvidiaDriverManagerCordonAnnotation, nvidiaUpgradeControllerCordonAnnotation)
return err
})
if err != nil {
return err
}
if acquired {
return nil
}
dm.log.Infof("Node %s was recordoned by an external actor before the claim could be recorded, waiting again", dm.config.nodeName)
}
}

// uncordonNode releases the cordon only when driver-manager applied it; anyone else's
// cordon is left in place.
func (dm *DriverManager) uncordonNode() error {
return retryOnAnyError(dm.ctx, dm.log, retryBackoff(defaultCordonRetries),
fmt.Sprintf("uncordon node %s", dm.config.nodeName),
func() error {
return dm.kubeClient.ReleaseNodeCordon(dm.config.nodeName,
nvidiaDriverManagerCordonAnnotation, nvidiaUpgradeControllerCordonAnnotation)
})
}

func (dm *DriverManager) nvDrainNode() error {
dm.log.Infof("Draining node %s of any GPU pods...", dm.config.nodeName)
drainOpts := kube.DrainOptions{
Expand All @@ -1250,21 +1320,6 @@ func (dm *DriverManager) isDriverAutoUpgradePolicyEnabled() bool {
return false
}

// cordonNode and uncordonNode wrap the kube client calls with this command's
// retry policy. The policy lives here rather than in the client so the client
// stays a thin wrapper over the Kubernetes API.
func (dm *DriverManager) cordonNode() error {
return retryOnAnyError(dm.ctx, dm.log, retryBackoff(defaultCordonRetries),
fmt.Sprintf("cordon node %s", dm.config.nodeName),
func() error { return dm.kubeClient.CordonNode(dm.config.nodeName) })
}

func (dm *DriverManager) uncordonNode() error {
return retryOnAnyError(dm.ctx, dm.log, retryBackoff(defaultCordonRetries),
fmt.Sprintf("uncordon node %s", dm.config.nodeName),
func() error { return dm.kubeClient.UncordonNode(dm.config.nodeName) })
}

func (dm *DriverManager) cleanupOnFailure() {
dm.log.Info("Performing cleanup on failure")
policyEnabled := dm.isDriverAutoUpgradePolicyEnabled()
Expand Down
222 changes: 213 additions & 9 deletions internal/kubernetes/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,16 @@ const (
computeDomainDRADriverName = "compute-domain." + nvidiaDomainPrefix
computeDomainDaemonConfigKind = "ComputeDomainDaemonConfig"

// Presence means a GPU component recorded this cordon; the value is the
// node's prior unschedulable state, not an ownership flag.
nodeInitialUnschedulableAnnotationKey = nvidiaDomainPrefix + "/" + "driver-manager.node-initial-state.unschedulable"

// Signals the upgrade controller (k8s-operator-libs) publishes while a node
// is mid-upgrade.
gpuUpgradeInitialUnschedulableAnnotationKey = nvidiaDomainPrefix + "/" + "gpu-driver-upgrade.node-initial-state.unschedulable"
gpuUpgradeWaitForSafeLoadAnnotationKey = nvidiaDomainPrefix + "/" + "gpu-driver-upgrade.driver-wait-for-safe-load"
gpuUpgradeStateLabelKey = nvidiaDomainPrefix + "/" + "gpu-driver-upgrade-state"

kubeClientPollInterval = 5 * time.Second
)

Expand All @@ -61,6 +69,9 @@ type Client struct {
log *logrus.Logger

clientset kubernetes.Interface
// pollInterval is the wait between retries for long-running watches such as
// WaitUntilNoExternalCordon. Zero means kubeClientPollInterval.
pollInterval time.Duration
}

// DrainOptions represents the option parameters that can passed to the drain.Helper struct
Expand Down Expand Up @@ -150,6 +161,198 @@ func (c *Client) GetNodeAnnotationValue(nodeName, annotation string) (string, er
return node.Annotations[annotation], nil
}

func annotationTrue(annotations map[string]string, key string) bool {
return annotations[key] == "true"
}

func annotationPresent(annotations map[string]string, key string) bool {
_, ok := annotations[key]
return ok
}

func gpuUpgradeInProgress(labels map[string]string) bool {
switch labels[gpuUpgradeStateLabelKey] {
case "cordon-required", "wait-for-jobs-required", "pod-deletion-required",
"drain-required", "node-maintenance-required", "post-maintenance-required",
"pod-restart-required", "validation-required", "uncordon-required",
"upgrade-failed":
Comment on lines +175 to +178

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Pending upgrade bypasses administrator cordon

When an administrator cordons a node labeled cordon-required, gpuUpgradeInProgress treats the pending state as an existing GPU cordon. Driver rotation proceeds during administrator maintenance.

Learn more

The upgrade-state label describes progress, not ownership of spec.unschedulable. cordon-required identifies the phase before the upgrade controller applies its cordon. A node already cordoned by an administrator can reach that phase without the controller owning the cordon. WaitUntilNoExternalCordon then returns, allowing uninstall to proceed despite the external cordon.

Example: An administrator cordons gpu-1; the controller sets gpu-driver-upgrade-state=cordon-required while waiting to cordon it. The manager starts and proceeds rather than waiting for the administrator to uncordon gpu-1.

Recommended fix: Do not accept pre-cordon upgrade phases as evidence of ownership. Use an atomic controller-owned claim when the controller actually cordons the node, and gate both waiting and acquisition on that claim.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

return true
Comment on lines +173 to +179

@devin-ai-integration devin-ai-integration Bot Nov 13, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Pending upgrade bypasses administrator cordon

On an administrator-cordoned node labeled cordon-required, gpuCordonClaimed authorizes rotation before the controller claims the cordon. The manager evicts pods despite the administrator's cordon and can later uncordon that node.

Learn more

An upgrade-state label identifies a stage of work, not the actor that set spec.unschedulable. The state cordon-required can be present before the upgrade controller applies a cordon. WaitUntilNoExternalCordon accepts the label as a GPU claim, and AcquireNodeCordon can then add driver-manager's claim to an administrator's cordon. The upgrade proceeds under the administrator's maintenance cordon; if the label clears before release, driver-manager can also uncordon it.

Example: An administrator cordons gpu-1 while its upgrade label is cordon-required. The driver manager immediately evicts GPU pods rather than waiting for the administrator to uncordon gpu-1.

Recommended fix: Do not infer cordon ownership from an upgrade state alone. Coordinate an explicit controller ownership marker with the upgrade controller, and require a confirmed owned cordon before allowing a pre-cordoned node through the wait or acquire checks.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

default:
return false
}
}

// gpuCordonClaimed reports whether a GPU Operator component owns the cordon.
// Initial-state keys are presence checks because their values are not ownership.
func gpuCordonClaimed(node *corev1.Node, claimAnnotation, peerClaimAnnotation string) bool {
if node == nil {
return false
}
annotations := node.Annotations
if annotationTrue(annotations, claimAnnotation) || annotationTrue(annotations, peerClaimAnnotation) {
return true
}
if annotationPresent(annotations, nodeInitialUnschedulableAnnotationKey) ||
annotationPresent(annotations, gpuUpgradeInitialUnschedulableAnnotationKey) ||
annotationPresent(annotations, gpuUpgradeWaitForSafeLoadAnnotationKey) {
return true
Comment on lines +195 to +198

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Recorded administrator cordons treated as GPU-owned

When an initial-state annotation says true, gpuCordonClaimed still accepts it as GPU ownership. The value records a pre-existing cordon, so uninstall proceeds on an administrator-cordoned node.

Learn more

The initial-state annotations record the value of spec.unschedulable before a GPU component performed its update. A value of true means the node was cordoned before that update; presence alone cannot identify the current cordon owner. The legacy CordonNode explicitly writes true for an already cordoned node. Treating either key as an unconditional claim bypasses both the initial wait and the acquisition guard.

Example: An administrator cordons gpu-1; an earlier manager writes driver-manager.node-initial-state.unschedulable=true. A new manager sees that annotation and rotates the driver without waiting for the administrator.

Recommended fix: Distinguish an initial-state recording from an active ownership claim. For legacy recordings, require evidence the node was schedulable before the component cordoned it; for controller ownership use a marker written atomically with its cordon.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +195 to +198

@devin-ai-integration devin-ai-integration Bot Nov 13, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Recorded administrator cordon becomes manager-owned

When CordonNode previously recorded true for an administrator-cordoned node, gpuCordonClaimed accepts that recording as ownership. The manager can claim, rotate, and uncordon the administrator's node.

Learn more

The existing CordonNode records the node's prior scheduling state, including true when the node was already cordoned. A recording of true therefore confirms the cordon was present before driver-manager touched the node; it is not a claim of ownership. The new presence check passes that node through WaitUntilNoExternalCordon, and AcquireNodeCordon can claim it. If no peer claim exists at release, ReleaseNodeCordon makes the administrator's node schedulable.

Example: An older driver-manager run records driver-manager.node-initial-state.unschedulable=true on an already cordoned node. A subsequent run sees that annotation, evicts workloads, and releases the administrator's cordon.

Recommended fix: Distinguish a true initial-state recording from ownership. Only an explicit validated owner claim or an initial state of false associated with an active manager cordon can justify proceeding or uncordoning; preserve the original state when releasing.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

}
return gpuUpgradeInProgress(node.Labels)
}

// gpuPeerOwnsCordon reports whether another GPU component still holds the cordon
// once this component drops its claim.
func gpuPeerOwnsCordon(node *corev1.Node, peerClaimAnnotation string) bool {
if node == nil {
return false
}
annotations := node.Annotations
if annotationTrue(annotations, peerClaimAnnotation) ||
annotationPresent(annotations, gpuUpgradeInitialUnschedulableAnnotationKey) ||
annotationPresent(annotations, gpuUpgradeWaitForSafeLoadAnnotationKey) {
return true
}
return gpuUpgradeInProgress(node.Labels)
}

func initialUnschedulableRecording(node *corev1.Node) (string, bool) {
_, recorded := node.Annotations[nodeInitialUnschedulableAnnotationKey]
if recorded && node.Spec.Unschedulable {
return "", false
}
return strconv.FormatBool(node.Spec.Unschedulable), true
}

// WaitUntilNoExternalCordon blocks until the node is schedulable or a GPU
// component has claimed the cordon, or until c.ctx is cancelled.
func (c *Client) WaitUntilNoExternalCordon(nodeName, claimAnnotation, peerClaimAnnotation string) error {
interval := kubeClientPollInterval
if c.pollInterval > 0 {
interval = c.pollInterval
}
loggedWaiting := false
return wait.PollUntilContextCancel(c.ctx, interval, true, func(ctx context.Context) (bool, error) {
node, err := c.clientset.CoreV1().Nodes().Get(ctx, nodeName, metav1.GetOptions{})
if err != nil {
c.log.Warnf("Failed to inspect cordon on node %s, retrying: %v", nodeName, err)
return false, nil
}
if !node.Spec.Unschedulable || gpuCordonClaimed(node, claimAnnotation, peerClaimAnnotation) {
if loggedWaiting {
c.log.Infof("External cordon on node %s has cleared, continuing driver rotation", nodeName)
}
return true, nil
}
if !loggedWaiting {
c.log.Infof("Node %s is already cordoned by an external actor, waiting until it is uncordoned or a GPU component claims the cordon", nodeName)
loggedWaiting = true
} else {
c.log.Infof("Still waiting for external cordon on node %s to be released", nodeName)
}
return false, nil
})
}

// AcquireNodeCordon atomically claims the cordon and marks the Node unschedulable.
// An externally owned cordon is left unchanged and returns false.
func (c *Client) AcquireNodeCordon(nodeName, claimAnnotation, peerClaimAnnotation string) (bool, error) {
acquired := false
err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
acquired = false
node, err := c.clientset.CoreV1().Nodes().Get(c.ctx, nodeName, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("failed to get node %s: %w", nodeName, err)
}

if node.Spec.Unschedulable && !gpuCordonClaimed(node, claimAnnotation, peerClaimAnnotation) {
return nil
}
if node.Spec.Unschedulable && annotationTrue(node.Annotations, claimAnnotation) {
acquired = true
return nil
}

annotations := map[string]any{
claimAnnotation: "true",
}
if initialState, write := initialUnschedulableRecording(node); write {
annotations[nodeInitialUnschedulableAnnotationKey] = initialState
}
if err := c.patchNodeSchedulingState(node, true, annotations); err != nil {
return err
}
acquired = true
return nil
})
if err != nil {
return false, fmt.Errorf("failed to acquire cordon claim on node %s: %w", nodeName, err)
}
return acquired, nil
}

// ReleaseNodeCordon atomically removes this component's claim, uncordoning the
// Node only when no other GPU component still owns the cordon.
func (c *Client) ReleaseNodeCordon(nodeName, claimAnnotation, peerClaimAnnotation string) error {
err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
node, err := c.clientset.CoreV1().Nodes().Get(c.ctx, nodeName, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("failed to get node %s: %w", nodeName, err)
}
if !annotationTrue(node.Annotations, claimAnnotation) {
return nil
Comment on lines +301 to +302

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Legacy driver cordons stay stuck

If a previous manager cordoned the node, ReleaseNodeCordon finds no new ownership claim and does nothing. A restarted manager's no-driver path completes without uncordoning that node.

Learn more

The previous CordonNode wrote only driver-manager.node-initial-state.unschedulable. On restart, the new uninstallDriver can take the no-driver path and call uncordonNode without acquiring a new claim. ReleaseNodeCordon then returns without cleaning up the old cordon. This leaves the node unschedulable after the successful driver rotation.

Example: The old manager cordons gpu-1, writes initial-state false, and its pod restarts during driver rotation. The replacement sees no loaded driver, finishes cleanup, but leaves gpu-1 cordoned because the new claim annotation is absent.

Recommended fix: Migrate legacy initial-state=false recordings into an ownership claim before the no-driver/skip paths, or explicitly release these legacy cordons when their prior state was schedulable; avoid claiming initial-state=true administrator cordons.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +301 to +302

@devin-ai-integration devin-ai-integration Bot Nov 13, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Legacy cordon remains after interrupted upgrade

If an older manager cordoned the node before unloading its driver, the replacement's ReleaseNodeCordon finds no new claim. The no-driver recovery path never acquires one, leaving the node cordoned after installation.

Learn more

Before this change, CordonNode recorded only driver-manager.node-initial-state.unschedulable, not the new ownership annotation. If the driver container restarts after the driver has been unloaded, uninstallDriver takes its no-driver path and later invokes uncordonNode without acquiring the new annotation. The release guard returns without removing the legacy cordon, so scheduling stays disabled.

Example: Version A cordons gpu-1 and records initial state false, then the driver unloads and the pod restarts with version B. Version B completes no-driver cleanup, but its uncordon call does nothing because gpu-driver-manager.node-cordoned is absent.

Recommended fix: Migrate legacy initial-state recordings when safe, especially false on an unschedulable node, before the no-driver release path. Ensure migration does not convert an initial true administrator cordon into a manager-owned cordon.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

}
if !node.Spec.Unschedulable {
return c.clearStaleClaim(node, claimAnnotation)
}

annotations := map[string]any{
claimAnnotation: nil,
}
unschedulable := gpuPeerOwnsCordon(node, peerClaimAnnotation)
if !unschedulable {
annotations[nodeInitialUnschedulableAnnotationKey] = nil
}
return c.patchNodeSchedulingState(node, unschedulable, annotations)
Comment on lines +311 to +315

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Shared cordon release restores wrong state

If a peer's cordon predates AcquireNodeCordon and clears first, ReleaseNodeCordon sets the node schedulable. It ignores the recorded initial true, undoing a cordon that existed before this manager acquired it.

Learn more

AcquireNodeCordon records node-initial-state.unschedulable=true when it joins a peer's existing cordon. ReleaseNodeCordon bases the final scheduling state only on whether that peer still has a claim. When the peer drops its claim first, the recorded initial state is still true, yet this function removes it and uncordons the node. This can also override an administrator's original cordon if the peer had cordoned an already unschedulable node.

Example: gpu-1 starts unschedulable under a controller claim. The manager acquires, recording true; the controller drops its claim. The manager releases and makes gpu-1 schedulable, although it was unschedulable before the manager joined.

Recommended fix: Preserve the initial scheduling state separately from peer ownership and restore it when the final claim is removed. Define how to handle peers that preserve their own initial state so the manager does not erase it.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

})
if err != nil {
return fmt.Errorf("failed to release cordon claim on node %s: %w", nodeName, err)
}
return nil
}

// ClearStaleCordonClaim removes a claim left on a schedulable Node, since
// `kubectl uncordon` does not clear annotations. A cordoned Node is unchanged.
func (c *Client) ClearStaleCordonClaim(nodeName, claimAnnotation string) error {
err := retry.RetryOnConflict(retry.DefaultBackoff, func() error {
node, err := c.clientset.CoreV1().Nodes().Get(c.ctx, nodeName, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("failed to get node %s: %w", nodeName, err)
}
if node.Spec.Unschedulable {
return nil
Comment on lines +331 to +332

@devin-ai-integration devin-ai-integration Bot Nov 13, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Reapplied administrator cordon inherits stale claim

If an administrator uncordons then recordons a node between manager runs, ClearStaleCordonClaim retains the old claim. The next run treats that administrator cordon as its own and can uncordon it.

Learn more

A successful AcquireNodeCordon leaves a manager ownership annotation on the Node until release. If the manager stops, an administrator can uncordon and later cordon that node again without changing the annotation. The next startup sees an already-cordoned Node and skips cleanup, although the active cordon is now external. WaitUntilNoExternalCordon and ReleaseNodeCordon then treat the stale marker as ownership and can remove the administrator's cordon.

Example: A failed upgrade leaves gpu-driver-manager.node-cordoned=true. An administrator uncordons gpu-1, then cordons it for maintenance before restarting the driver pod. The manager skips cleanup and later uncordons gpu-1.

Recommended fix: Do not treat an indefinitely persistent claim as evidence of ownership across an unobserved uncordon/recordon cycle. Use a controller-coordinated ownership protocol or an observable cordon generation/lease, and avoid releasing the node when ownership cannot be established.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

}
if !annotationPresent(node.Annotations, claimAnnotation) &&
!annotationPresent(node.Annotations, nodeInitialUnschedulableAnnotationKey) {
return nil
}
return c.clearStaleClaim(node, claimAnnotation)
})
if err != nil {
return fmt.Errorf("failed to clear stale cordon claim on node %s: %w", nodeName, err)
}
return nil
}

// clearStaleClaim drops the claim from a schedulable Node. The resourceVersion
// precondition makes a concurrent cordon conflict instead of being undone.
func (c *Client) clearStaleClaim(node *corev1.Node, claimAnnotation string) error {
c.log.Infof("Node %s is schedulable but carries a cordon claim from an earlier rotation, clearing it", node.Name)
return c.patchNodeSchedulingState(node, false, map[string]any{
claimAnnotation: nil,
nodeInitialUnschedulableAnnotationKey: nil,
})
}

// CordonNode atomically records a Node's initial schedulable state and marks it
// Unschedulable. An existing recording on an Unschedulable Node is retained so
// that restarting driver-manager does not lose the initial state.
Expand All @@ -159,14 +362,15 @@ func (c *Client) CordonNode(nodeName string) error {
return fmt.Errorf("failed to get node %s: %w", nodeName, err)
}

_, recorded := node.Annotations[nodeInitialUnschedulableAnnotationKey]
if node.Spec.Unschedulable && recorded {
initialState, write := initialUnschedulableRecording(node)
if node.Spec.Unschedulable && !write {
return nil
}

initialState := strconv.FormatBool(node.Spec.Unschedulable)
c.log.Infof("Cordoning node %s and recording initial state in annotation %s=%s", nodeName, nodeInitialUnschedulableAnnotationKey, initialState)
return c.patchNodeSchedulingState(node, true, initialState)
return c.patchNodeSchedulingState(node, true, map[string]any{
nodeInitialUnschedulableAnnotationKey: initialState,
})
}

// UncordonNode atomically marks a Node as schedulable and removes the initial
Expand All @@ -184,16 +388,16 @@ func (c *Client) UncordonNode(nodeName string) error {
}

c.log.Infof("Uncordoning node %s", nodeName)
return c.patchNodeSchedulingState(node, false, nil)
return c.patchNodeSchedulingState(node, false, map[string]any{
nodeInitialUnschedulableAnnotationKey: nil,
})
}

func (c *Client) patchNodeSchedulingState(node *corev1.Node, unschedulable bool, annotationValue any) error {
func (c *Client) patchNodeSchedulingState(node *corev1.Node, unschedulable bool, annotations map[string]any) error {
patch := map[string]any{
"metadata": map[string]any{
"resourceVersion": node.ResourceVersion,
"annotations": map[string]any{
nodeInitialUnschedulableAnnotationKey: annotationValue,
},
"annotations": annotations,
},
"spec": map[string]any{
"unschedulable": unschedulable,
Expand Down
Loading
Loading