Skip to content

Commit 38cc3e7

Browse files
martbhellgyuho
andauthored
fix(containerd): detect NVIDIA runtime under containerd v2 plugin path and via imports (#1242)
The nvidia runtime check searches /etc/containerd/config.toml for the literal `plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia`, which is the containerd 1.x plugin path. containerd 2.x uses `io.containerd.cri.v1.runtime`, so the check raises a false positive on every v2 host even when the nvidia runtime is configured correctly. Two changes: - Accept either plugin path (1.x grpc.v1.cri or 2.x cri.v1.runtime). - Follow `imports = [...]` directives in the main config so drop-in files (e.g. /etc/containerd/conf.d/99-nvidia.toml written by nvidia-container-toolkit-daemonset on recent gpu-operator versions) are inspected too. Read errors on individual imports are ignored so the existing substring checks still run against whatever could be read. Adds unit tests for the v2 plugin path, the new constant, and the import-following helper. The error: ``` May 07 12:01:00 cluster-2 gpud[3478]: {"level":"warn","ts":"2026-05-07T12:01:00.934Z","caller":"log/log.go:175","msg":"nvidia GPUs found but nvidia-container-toolkit pod is not found"} ``` - [ ] Marking as draft I also haven't tested this (yet) and this is claude coding. With this kind of containerd setup: ``` $ containerd --version containerd containerd.io v2.2.3 77c84241c7cbdd9b4eca2591793e3d4f4317c590 $ find /etc/containerd/ -type f /etc/containerd/config.toml /etc/containerd/config.d/root-on-nvme.toml $ find /etc/containerd/ -type f|xargs cat imports = ["/etc/containerd/config.d/*.toml", "/etc/containerd/conf.d/*.toml"] version = 3 [metrics] address = "0.0.0.0:1338" grpc_histogram = true root = "/mnt/local_disk/containerd" ``` Co-authored-by: Gyuho Lee <6799218+gyuho@users.noreply.github.com>
1 parent aad0c01 commit 38cc3e7

3 files changed

Lines changed: 115 additions & 5 deletions

File tree

components/containerd/component.go

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"encoding/json"
88
"fmt"
99
"os"
10+
"path/filepath"
11+
"regexp"
1012
"strings"
1113
"sync"
1214
"time"
@@ -34,11 +36,19 @@ const (
3436

3537
socketMissingConsecutiveThreshold = 5
3638

37-
// Containerd config strings to check for NVIDIA runtime configuration
38-
containerdConfigNvidiaDefaultRuntime = `default_runtime_name = "nvidia"`
39-
containerdConfigNvidiaRuntimePlugin = `plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia`
39+
// Containerd config strings to check for NVIDIA runtime configuration.
40+
// containerd 1.x uses the "io.containerd.grpc.v1.cri" plugin path; 2.x
41+
// uses "io.containerd.cri.v1.runtime". Either is acceptable.
42+
containerdConfigNvidiaDefaultRuntime = `default_runtime_name = "nvidia"`
43+
containerdConfigNvidiaRuntimePlugin = `plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia`
44+
containerdConfigNvidiaRuntimePluginV2 = `plugins."io.containerd.cri.v1.runtime".containerd.runtimes.nvidia`
4045
)
4146

47+
// containerdConfigImportsRe matches `imports = [...]` directives in a
48+
// containerd config so any drop-in files (e.g. /etc/containerd/conf.d/*.toml
49+
// written by nvidia-container-toolkit-daemonset) can be inspected too.
50+
var containerdConfigImportsRe = regexp.MustCompile(`(?m)^\s*imports\s*=\s*\[([^\]]*)\]`)
51+
4252
var _ components.Component = &component{}
4353

4454
type component struct {
@@ -363,13 +373,17 @@ func (c *component) Check() components.CheckResult {
363373
// been running long enough
364374
if elapsed > c.containerToolkitCreationThreshold {
365375
config, err := c.getContainerdConfigFunc()
376+
if err == nil {
377+
config = appendImportedContainerdConfigs(config)
378+
}
379+
hasNvidiaPlugin := bytes.Contains(config, []byte(containerdConfigNvidiaRuntimePlugin)) ||
380+
bytes.Contains(config, []byte(containerdConfigNvidiaRuntimePluginV2))
366381
switch {
367382
case err != nil:
368383
reason := "error getting containerd config"
369384
cr.appendReason(reason)
370385
log.Logger.Warnw(reason)
371-
case !bytes.Contains(config, []byte(containerdConfigNvidiaDefaultRuntime)) ||
372-
!bytes.Contains(config, []byte(containerdConfigNvidiaRuntimePlugin)):
386+
case !bytes.Contains(config, []byte(containerdConfigNvidiaDefaultRuntime)) || !hasNvidiaPlugin:
373387
reason := fmt.Sprintf("nvidia-container-toolkit pod is running but %s is missing NVIDIA runtime configuration", defaultContainerdConfigPath)
374388
cr.appendReason(reason)
375389
log.Logger.Warnw(reason)
@@ -499,6 +513,37 @@ func (cr *checkResult) HealthStates() apiv1.HealthStates {
499513
return apiv1.HealthStates{state}
500514
}
501515

516+
// appendImportedContainerdConfigs returns config with the contents of any
517+
// files referenced by an "imports = [...]" directive appended. Errors
518+
// reading individual imports are ignored so the substring checks still
519+
// run against whatever could be read.
520+
func appendImportedContainerdConfigs(config []byte) []byte {
521+
m := containerdConfigImportsRe.FindSubmatch(config)
522+
if len(m) < 2 {
523+
return config
524+
}
525+
out := config
526+
for _, raw := range strings.Split(string(m[1]), ",") {
527+
pattern := strings.Trim(strings.TrimSpace(raw), `"'`)
528+
if pattern == "" {
529+
continue
530+
}
531+
matches, err := filepath.Glob(pattern)
532+
if err != nil {
533+
continue
534+
}
535+
for _, f := range matches {
536+
b, err := os.ReadFile(f)
537+
if err != nil {
538+
continue
539+
}
540+
out = append(out, '\n')
541+
out = append(out, b...)
542+
}
543+
}
544+
return out
545+
}
546+
502547
func danglingPodCount(containerdPods []PodSandbox, kubeletPods []componentkubelet.PodStatus) int {
503548
var danglingCount int
504549
if len(kubeletPods) == 0 {

components/containerd/component_config_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package containerd
22

33
import (
4+
"os"
5+
"path/filepath"
46
"testing"
57

68
"github.com/stretchr/testify/assert"
@@ -11,17 +13,50 @@ func TestContainerdConfigConstants(t *testing.T) {
1113
// Test that the constants match the expected values
1214
assert.Equal(t, `default_runtime_name = "nvidia"`, containerdConfigNvidiaDefaultRuntime)
1315
assert.Equal(t, `plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia`, containerdConfigNvidiaRuntimePlugin)
16+
assert.Equal(t, `plugins."io.containerd.cri.v1.runtime".containerd.runtimes.nvidia`, containerdConfigNvidiaRuntimePluginV2)
1417

1518
// Test that the constants can be used as byte slices
1619
assert.NotNil(t, []byte(containerdConfigNvidiaDefaultRuntime))
1720
assert.NotNil(t, []byte(containerdConfigNvidiaRuntimePlugin))
21+
assert.NotNil(t, []byte(containerdConfigNvidiaRuntimePluginV2))
1822

1923
// Test that the constants have the expected content
2024
assert.Contains(t, containerdConfigNvidiaDefaultRuntime, "nvidia")
2125
assert.Contains(t, containerdConfigNvidiaRuntimePlugin, "nvidia")
2226
assert.Contains(t, containerdConfigNvidiaRuntimePlugin, "plugins")
2327
assert.Contains(t, containerdConfigNvidiaRuntimePlugin, "containerd")
2428
assert.Contains(t, containerdConfigNvidiaRuntimePlugin, "runtimes")
29+
assert.Contains(t, containerdConfigNvidiaRuntimePluginV2, "nvidia")
30+
assert.Contains(t, containerdConfigNvidiaRuntimePluginV2, "io.containerd.cri.v1.runtime")
31+
}
32+
33+
// TestAppendImportedContainerdConfigs verifies that imports referenced in a
34+
// containerd config are read and appended so substring checks see drop-in files.
35+
func TestAppendImportedContainerdConfigs(t *testing.T) {
36+
dir := t.TempDir()
37+
dropIn := filepath.Join(dir, "99-nvidia.toml")
38+
dropInBody := `[plugins."io.containerd.cri.v1.runtime".containerd]
39+
default_runtime_name = "nvidia"
40+
[plugins."io.containerd.cri.v1.runtime".containerd.runtimes.nvidia]
41+
runtime_type = "io.containerd.runc.v2"
42+
`
43+
if err := os.WriteFile(dropIn, []byte(dropInBody), 0o644); err != nil {
44+
t.Fatalf("write drop-in: %v", err)
45+
}
46+
47+
main := []byte("version = 3\nimports = [\"" + filepath.Join(dir, "*.toml") + "\"]\n")
48+
out := appendImportedContainerdConfigs(main)
49+
50+
assert.Contains(t, string(out), containerdConfigNvidiaDefaultRuntime)
51+
assert.Contains(t, string(out), containerdConfigNvidiaRuntimePluginV2)
52+
}
53+
54+
// TestAppendImportedContainerdConfigs_NoImports returns the input unchanged
55+
// when no imports directive is present.
56+
func TestAppendImportedContainerdConfigs_NoImports(t *testing.T) {
57+
in := []byte("version = 3\n[metrics]\n address = \"0.0.0.0:1338\"\n")
58+
out := appendImportedContainerdConfigs(in)
59+
assert.Equal(t, string(in), string(out))
2560
}
2661

2762
// TestContainerdConfigWithBothNvidiaSettings tests configs that should contain both nvidia settings

components/containerd/component_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2063,6 +2063,36 @@ func TestNVMLValidationWithContainerToolkit(t *testing.T) {
20632063
expectedHealth: apiv1.HealthStateTypeHealthy,
20642064
expectedReason: "ok",
20652065
},
2066+
{
2067+
name: "nvml with containerd v2 nvidia plugin path",
2068+
nvmlInstance: &mockNVMLInstance{
2069+
nvmlExists: true,
2070+
productName: "Tesla V100",
2071+
},
2072+
getContainerdConfigFunc: func() ([]byte, error) {
2073+
config := `default_runtime_name = "nvidia"
2074+
[plugins."io.containerd.cri.v1.runtime".containerd.runtimes.nvidia]
2075+
runtime_type = "io.containerd.runc.v2"`
2076+
return []byte(config), nil
2077+
},
2078+
pods: []PodSandbox{
2079+
{
2080+
Name: "nvidia-container-toolkit-daemonset-v2",
2081+
State: "SANDBOX_READY",
2082+
Containers: []PodSandboxContainerStatus{
2083+
{
2084+
Name: "nvidia-container-toolkit-ctr",
2085+
State: "CONTAINER_RUNNING",
2086+
CreatedAt: time.Now().Add(-15 * time.Minute).UnixNano(),
2087+
},
2088+
},
2089+
},
2090+
},
2091+
getTimeNowFunc: time.Now,
2092+
containerToolkitCreationThreshold: 10 * time.Minute,
2093+
expectedHealth: apiv1.HealthStateTypeHealthy,
2094+
expectedReason: "ok",
2095+
},
20662096
{
20672097
name: "nvml without nvidia config but container toolkit present and running long enough",
20682098
nvmlInstance: &mockNVMLInstance{

0 commit comments

Comments
 (0)