Skip to content

Commit 6df1212

Browse files
committed
add support for resetting third party gpu client pods
Signed-off-by: Tariq Ibrahim <tibrahim@nvidia.com>
1 parent ae3f46d commit 6df1212

3 files changed

Lines changed: 167 additions & 17 deletions

File tree

cmd/driver-manager/main.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"github.com/sirupsen/logrus"
3434
"github.com/urfave/cli/v2"
3535
"golang.org/x/sys/unix"
36+
corev1 "k8s.io/api/core/v1"
3637

3738
"github.com/NVIDIA/k8s-driver-manager/internal/info"
3839
kube "github.com/NVIDIA/k8s-driver-manager/internal/kubernetes"
@@ -62,6 +63,17 @@ const (
6263
nvidiaSandboxValidatorDeployLabel = nvidiaDomainPrefix + "/" + "gpu.deploy.sandbox-validator"
6364
nvidiaSandboxDevicePluginDeployLabel = nvidiaDomainPrefix + "/" + "gpu.deploy.sandbox-device-plugin"
6465
nvidiaVGPUDeviceManagerDeployLabel = nvidiaDomainPrefix + "/" + "gpu.deploy.vgpu-device-manager"
66+
67+
nvidiaGPUClientAnnotation = nvidiaDomainPrefix + "/" + "gpu.client"
68+
nvidiaTaintKey = nvidiaDomainPrefix + "/" + "gpu"
69+
)
70+
71+
var (
72+
driverManagerTaint = corev1.Taint{
73+
Key: nvidiaTaintKey,
74+
Value: "k8s-driver-manager",
75+
Effect: corev1.TaintEffectNoSchedule,
76+
}
6577
)
6678

6779
// Configuration holds all the configuration from environment variables
@@ -350,6 +362,11 @@ func (dm *DriverManager) uninstallDriver() error {
350362

351363
// Check if driver is loaded and cleanup if needed
352364
if dm.isDriverLoaded() {
365+
// Apply Taint on node to prevent pods from being scheduled during a driver unload cycle
366+
if err := dm.kubeClient.TaintNode(driverManagerTaint, dm.config.nodeName); err != nil {
367+
return fmt.Errorf("failed to taint Node %s: %w", dm.config.nodeName, err)
368+
}
369+
353370
if err := dm.cleanupDriver(); err != nil {
354371
if dm.isAutoDrainEnabled() {
355372
dm.log.Info("Unable to cleanup driver modules, attempting again with node drain...")
@@ -368,6 +385,9 @@ func (dm *DriverManager) uninstallDriver() error {
368385
return fmt.Errorf("failed to uninstall nvidia driver components: %w", err)
369386
}
370387
}
388+
if err := dm.kubeClient.UntaintNode(driverManagerTaint, dm.config.nodeName); err != nil {
389+
return fmt.Errorf("failed to untaint Node %s: %w", dm.config.nodeName, err)
390+
}
371391
dm.log.Info("Successfully uninstalled nvidia driver components")
372392
}
373393

@@ -650,6 +670,27 @@ func (dm *DriverManager) waitForPodsToTerminate() error {
650670
}
651671
}
652672

673+
dm.log.Info("Checking if there are additional gpu clients that need to be restarted...")
674+
675+
gpuClientPods, err := dm.kubeClient.ListPodsOnNodeWithAnnotation(nvidiaGPUClientAnnotation, nodeName)
676+
if err != nil {
677+
dm.log.Errorf("Failed to list the gpu client pods: %v", err)
678+
return err
679+
}
680+
681+
if len(gpuClientPods) > 0 {
682+
dm.log.Info("Found extra gpu-client pods that need to be reset.")
683+
684+
for _, pod := range gpuClientPods {
685+
dm.log.Infof("Deleting pod %s/%s", pod.Namespace, pod.Name)
686+
687+
if err = dm.kubeClient.DeletePod(pod.Name, pod.Namespace); err != nil {
688+
dm.log.Errorf("Failed to delete pod %s/%s: %v", pod.Namespace, pod.Name, err)
689+
return err
690+
}
691+
}
692+
}
693+
653694
return nil
654695
}
655696

@@ -945,6 +986,11 @@ func (dm *DriverManager) cleanupOnFailure() {
945986
policyEnabled := dm.isDriverAutoUpgradePolicyEnabled()
946987
managerCanEvict := !policyEnabled && (dm.config.enableGPUPodEviction || dm.config.enableAutoDrain)
947988

989+
// Ensure that the taint previously applied is removed during cleanup
990+
if err := dm.kubeClient.UntaintNode(driverManagerTaint, dm.config.nodeName); err != nil {
991+
dm.log.Warnf("failed to untaint Node %s: %v", dm.config.nodeName, err)
992+
}
993+
948994
switch {
949995
case managerCanEvict:
950996
if err := dm.kubeClient.UncordonNode(dm.config.nodeName); err != nil {

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/stretchr/testify v1.11.1
1010
github.com/urfave/cli/v2 v2.27.7
1111
golang.org/x/sys v0.43.0
12+
gopkg.in/evanphx/json-patch.v4 v4.12.0
1213
k8s.io/api v0.33.2
1314
k8s.io/apimachinery v0.33.2
1415
k8s.io/client-go v0.33.2
@@ -68,7 +69,6 @@ require (
6869
golang.org/x/text v0.23.0 // indirect
6970
golang.org/x/time v0.9.0 // indirect
7071
google.golang.org/protobuf v1.36.5 // indirect
71-
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
7272
gopkg.in/inf.v0 v0.9.1 // indirect
7373
gopkg.in/yaml.v3 v3.0.1 // indirect
7474
k8s.io/cli-runtime v0.33.2 // indirect

internal/kubernetes/client.go

Lines changed: 120 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"time"
2626

2727
"github.com/sirupsen/logrus"
28+
jsonpatch "gopkg.in/evanphx/json-patch.v4"
2829
corev1 "k8s.io/api/core/v1"
2930
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3031
"k8s.io/apimachinery/pkg/labels"
@@ -108,22 +109,7 @@ func (c *Client) UpdateNodeLabels(nodeName string, nodeLabels map[string]string)
108109
return fmt.Errorf("failed to marshal patch: %w", err)
109110
}
110111

111-
backoff := wait.Backoff{
112-
Duration: time.Second,
113-
Factor: 2.0,
114-
Jitter: 0.2,
115-
Steps: 7,
116-
}
117-
118-
return retry.OnError(backoff, func(err error) bool {
119-
return true
120-
}, func() error {
121-
_, err := c.clientset.CoreV1().Nodes().Patch(c.ctx, nodeName, types.StrategicMergePatchType, patchBytes, metav1.PatchOptions{})
122-
if err != nil {
123-
c.log.Warnf("Failed to update labels on node %s, retrying: %v", nodeName, err)
124-
}
125-
return err
126-
})
112+
return c.patchNodeWithRetry(nodeName, patchBytes, types.StrategicMergePatchType)
127113
}
128114

129115
// GetNodeAnnotationValue returns the annotation value given a node name and annotation key
@@ -164,6 +150,58 @@ func (c *Client) UncordonNode(nodeName string) error {
164150
return drain.RunCordonOrUncordon(drainHelper, node, false)
165151
}
166152

153+
// TaintNode applies a taint on a Node marking it as Unschedulable
154+
func (c *Client) TaintNode(taint corev1.Taint, nodeName string) error {
155+
c.log.Infof("Tainting node %s", nodeName)
156+
157+
node, err := c.clientset.CoreV1().Nodes().Get(c.ctx, nodeName, metav1.GetOptions{})
158+
if err != nil {
159+
return fmt.Errorf("failed to get node %s: %w", nodeName, err)
160+
}
161+
162+
original := node.DeepCopy()
163+
164+
// First, we check if the taint already exists. If yes, we skip the node patch operation
165+
for _, nodeTaint := range node.Spec.Taints {
166+
if taintsEqual(nodeTaint, taint) {
167+
c.log.Infof("Taint has already been applied. Skipping node taint...")
168+
return nil
169+
}
170+
}
171+
172+
node.Spec.Taints = append(node.Spec.Taints, taint)
173+
174+
return c.applyNodeDiff(original, node)
175+
}
176+
177+
// UntaintNode removes the taint on a Node
178+
func (c *Client) UntaintNode(taint corev1.Taint, nodeName string) error {
179+
c.log.Infof("Untainting node %s", nodeName)
180+
181+
node, err := c.clientset.CoreV1().Nodes().Get(c.ctx, nodeName, metav1.GetOptions{})
182+
if err != nil {
183+
return fmt.Errorf("failed to get node %s: %w", nodeName, err)
184+
}
185+
186+
original := node.DeepCopy()
187+
188+
found := false
189+
for i, nodeTaint := range node.Spec.Taints {
190+
if taintsEqual(nodeTaint, taint) {
191+
node.Spec.Taints = append(node.Spec.Taints[:i], node.Spec.Taints[i+1:]...)
192+
found = true
193+
break
194+
}
195+
}
196+
197+
if !found {
198+
c.log.Infof("Taint not found. Skipping node untaint...")
199+
return nil
200+
}
201+
202+
return c.applyNodeDiff(original, node)
203+
}
204+
167205
// WaitForPodTermination will wait for the termination of pods matching labels from the selectorMap on the node with the specified namespace.
168206
// It will continue to wait until the specified timeout elapses
169207
func (c *Client) WaitForPodTermination(selectorMap map[string]string, namespace, nodeName string, timeout time.Duration) error {
@@ -183,6 +221,29 @@ func (c *Client) WaitForPodTermination(selectorMap map[string]string, namespace,
183221
})
184222
}
185223

224+
// ListPodsOnNodeWithAnnotation will list all pods matching a specified annotation on a node
225+
func (c *Client) ListPodsOnNodeWithAnnotation(annotationKey string, nodeName string) ([]corev1.Pod, error) {
226+
227+
podList, err := c.clientset.CoreV1().Pods(corev1.NamespaceAll).List(c.ctx, metav1.ListOptions{
228+
FieldSelector: "spec.nodeName=" + nodeName,
229+
})
230+
if err != nil {
231+
return nil, err
232+
}
233+
var result []corev1.Pod
234+
for _, pod := range podList.Items {
235+
if _, exists := pod.Annotations[annotationKey]; exists {
236+
result = append(result, pod)
237+
}
238+
}
239+
return result, nil
240+
}
241+
242+
// DeletePod will delete a pid with the name "podName"
243+
func (c *Client) DeletePod(podName, namespace string) error {
244+
return c.clientset.CoreV1().Pods(namespace).Delete(c.ctx, podName, metav1.DeleteOptions{})
245+
}
246+
186247
// DrainNode drains a Node given a Node name and a set of drain option parameters
187248
func (c *Client) DrainNode(nodeName string, drainOpts DrainOptions) error {
188249
c.log.Infof("Draining node %s", nodeName)
@@ -293,3 +354,46 @@ func gpuPodSpecFilter(pod corev1.Pod) bool {
293354
}
294355
return false
295356
}
357+
358+
func (c *Client) applyNodeDiff(currentNode *corev1.Node, desiredNode *corev1.Node) error {
359+
currentNodeJSON, err := json.Marshal(currentNode)
360+
if err != nil {
361+
return fmt.Errorf("failed to marshal node object %v: %w", currentNode, err)
362+
}
363+
desiredNodeJSON, err := json.Marshal(desiredNode)
364+
if err != nil {
365+
return fmt.Errorf("failed to marshal node object %v: %w", desiredNode, err)
366+
}
367+
368+
patch, err := jsonpatch.CreateMergePatch(currentNodeJSON, desiredNodeJSON)
369+
if err != nil {
370+
return fmt.Errorf("failed to create merge-patch: %w", err)
371+
}
372+
return c.patchNodeWithRetry(desiredNode.Name, patch, types.MergePatchType)
373+
}
374+
375+
func (c *Client) patchNodeWithRetry(nodeName string, patch []byte, patchType types.PatchType) error {
376+
377+
backoff := wait.Backoff{
378+
Duration: time.Second,
379+
Factor: 2.0,
380+
Jitter: 0.2,
381+
Steps: 7,
382+
}
383+
return retry.OnError(backoff, func(err error) bool {
384+
return true
385+
}, func() error {
386+
_, err := c.clientset.CoreV1().Nodes().Patch(c.ctx, nodeName, patchType, patch, metav1.PatchOptions{})
387+
if err != nil {
388+
c.log.Warnf("Failed to patch node %s, retrying: %v", nodeName, err)
389+
}
390+
return err
391+
})
392+
}
393+
394+
func taintsEqual(t1 corev1.Taint, t2 corev1.Taint) bool {
395+
keysEqual := t1.Key == t2.Key
396+
valuesEqual := t1.Value == t2.Value
397+
effectsEqual := t1.Effect == t2.Effect
398+
return keysEqual && valuesEqual && effectsEqual
399+
}

0 commit comments

Comments
 (0)