Add e2e test for NVIDIA device plugin DaemonSet deployment with Renovate auto-update#7984
Conversation
PR Title Lint Failed ❌Current Title: Your PR title doesn't follow the expected format. Please update your PR title to follow one of these patterns: Conventional Commits Format:
Guidelines:
Examples:
Please update your PR title and the lint check will run again automatically. |
There was a problem hiding this comment.
Pull request overview
Adds coverage in the e2e suite for the “upstream” NVIDIA device plugin deployment model (as a Kubernetes DaemonSet) and configures Renovate to keep the test’s container image tag up to date.
Changes:
- Introduces a new Ubuntu 22.04 GPU e2e scenario that deploys the NVIDIA device plugin as a DaemonSet and validates GPU advertisement + scheduling.
- Adds a Renovate custom regex manager to update docker image tags referenced in e2e Go tests via
// renovate:comments. - Extends the existing Renovate grouping for
nvidia-device-pluginto also include the MCR upstream device-plugin image.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| e2e/scenario_gpu_daemonset_test.go | New e2e GPU scenario that validates DaemonSet-based NVIDIA device plugin deployment (not systemd). |
| .github/renovate.json | Adds a custom manager to update image tags in e2e Go tests and groups the MCR image with existing NVIDIA device plugin updates. |
| // Check if the service exists and is inactive | ||
| // Using "is-active" which returns non-zero if not active | ||
| result := execScriptOnVMForScenario(ctx, s, "systemctl is-active nvidia-device-plugin.service 2>/dev/null || echo 'not-running'") | ||
| output := strings.TrimSpace(result.stdout) | ||
|
|
||
| // The service should either not exist or be inactive | ||
| if output == "active" { | ||
| s.T.Fatalf("nvidia-device-plugin.service is unexpectedly running - this test requires the systemd service to be disabled") |
There was a problem hiding this comment.
The systemctl is-active ... || echo 'not-running' pattern discards both the real service state (e.g., "inactive", "failed", "activating") and the exit code, so the test can incorrectly pass even if the service is transitioning/running (e.g., "activating"). Capture the actual systemctl is-active output/exit status and fail on any running-like state (active/activating), or use execScriptOnVMForScenarioValidateExitCode with the expected non-active exit codes.
| // Check if the service exists and is inactive | |
| // Using "is-active" which returns non-zero if not active | |
| result := execScriptOnVMForScenario(ctx, s, "systemctl is-active nvidia-device-plugin.service 2>/dev/null || echo 'not-running'") | |
| output := strings.TrimSpace(result.stdout) | |
| // The service should either not exist or be inactive | |
| if output == "active" { | |
| s.T.Fatalf("nvidia-device-plugin.service is unexpectedly running - this test requires the systemd service to be disabled") | |
| // Check the current service state using "is-active". | |
| // This will return "active", "inactive", "failed", "activating", "unknown", etc. | |
| result := execScriptOnVMForScenario(ctx, s, "systemctl is-active nvidia-device-plugin.service 2>/dev/null") | |
| output := strings.TrimSpace(result.stdout) | |
| // The service should either not exist or be in a non-running state. | |
| // Treat both "active" and "activating" as failures, since the service | |
| // must not be running when validating the DaemonSet-based deployment. | |
| if output == "active" || output == "activating" { | |
| s.T.Fatalf("nvidia-device-plugin.service is unexpectedly %s - this test requires the systemd service to be disabled", output) |
| maxLen := 63 | ||
| name := prefix + nodeName | ||
| if len(name) > maxLen { | ||
| name = name[:maxLen] | ||
| } |
There was a problem hiding this comment.
nvidiaDevicePluginDaemonsetName truncates the name to 63 chars but doesn’t ensure the truncated result still conforms to DNS label rules (must end with an alphanumeric). If truncation cuts on a '-', Kubernetes can reject the object/label value. Mirror the existing truncatePodName behavior by trimming trailing '-' after truncation (and apply any needed sanitization).
| _ = s.Runtime.Cluster.Kube.Typed.AppsV1().DaemonSets(ds.Namespace).Delete( | ||
| deleteCtx, | ||
| ds.Name, | ||
| metav1.DeleteOptions{}, | ||
| ) |
There was a problem hiding this comment.
The test deletes an existing DaemonSet and then recreates it without waiting for deletion to finish. If a prior run left it terminating, the create can fail with AlreadyExists / "object is being deleted" and make the test flaky. Consider polling until a GET returns NotFound (or using an update-based approach) before creating.
| "matchPackageNames": [ | ||
| "nvidia-device-plugin" | ||
| "nvidia-device-plugin", | ||
| "oss/v2/nvidia/k8s-device-plugin" | ||
| ], |
There was a problem hiding this comment.
The PR description says a new Renovate custom manager was added to auto-update container image versions in e2e Go test files (via // renovate: comments), but this renovate.json change only updates package grouping. There is still no customManagers entry that targets e2e/**/*.go or parses // renovate: lines, so Renovate will not update Go test image strings as described. Either add the intended custom manager configuration or update the PR description to match the actual approach (version coming from components.json).
| jsonBytes, err := os.ReadFile(componentsPath) | ||
| if err != nil { | ||
| return "" | ||
| } |
There was a problem hiding this comment.
GetE2EContainerImage returns an empty string if reading/parsing components.json fails or the image entry is missing. In practice this causes later Kubernetes errors like “invalid image name” and hides the real root cause. Consider returning (string, error) (or failing fast) and have callers require.NoError/require.NotEmpty so test failures point to the missing/misconfigured components.json entry.
| Name: "nvidia-device-plugin-ctr", | ||
| Image: getNvidiaDevicePluginImage(), |
There was a problem hiding this comment.
The DaemonSet uses Image: getNvidiaDevicePluginImage() without asserting it’s non-empty/valid. If components.json lookup fails, the pod will be created with an empty image and the test will fail with a confusing scheduling/pull error. Add an early require.NotEmpty (or propagate an error from GetE2EContainerImage) so failures clearly indicate a missing/misconfigured E2EContainerImages entry.
| Name: "nvidia-device-plugin-ctr", | |
| Image: getNvidiaDevicePluginImage(), | |
| Name: "nvidia-device-plugin-ctr", | |
| Image: func() string { | |
| img := getNvidiaDevicePluginImage() | |
| require.NotEmpty(t, img, "nvidia device plugin image must be configured in E2EContainerImages (components.json entry %q)", nvidiaDevicePluginImageName) | |
| return img | |
| }(), |
| "version": { | ||
| "renovateTag": "registry=https://mcr.microsoft.com, name=oss/v2/nvidia/k8s-device-plugin", | ||
| "latestVersion": "v0.18.2-1" | ||
| } |
There was a problem hiding this comment.
The PR description says the test uses mcr.microsoft.com/oss/v2/nvidia/k8s-device-plugin:v0.18.2, but components.json pins latestVersion to v0.18.2-1. Please reconcile the version/tag format (either update the description or adjust latestVersion) to avoid confusion and to ensure the referenced tag is the one actually being tested.
e2e/scenario_gpu_daemonset_test.go
Outdated
| // NVIDIA device plugin deployment. | ||
| func Test_Ubuntu2204_NvidiaDevicePlugin_Daemonset(t *testing.T) { | ||
| RunScenario(t, &Scenario{ | ||
| Description: "Tests that NVIDIA device plugin works when deployed as a DaemonSet (not systemd service)", |
There was a problem hiding this comment.
should we just add the device plugin daemonset to all the exisiting unmanaged scenarios? instead of adding a new one?
…ate tracking - Add E2EContainerImages section to components.json for container images used only in e2e tests (not cached on VHD) - Add nvidia k8s-device-plugin container image to E2EContainerImages - Add GetGPUContainerImage and GetE2EContainerImage helpers in e2e/components - Update scenario_gpu_daemonset_test.go to read version from components.json instead of hardcoding it - Move DaemonSet validation inline into Test_Ubuntu2204_GPUNC and Test_Ubuntu2204_GPUA10 to avoid spinning up an additional GPU VM - Add schema definition for E2EContainerImages in components.cue - Add Renovate package rule for oss/v2/nvidia/k8s-device-plugin
a365ad1 to
24402e6
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
You can also share your feedback on Copilot code review. Take the survey.
| func GetGPUContainerImage(downloadURLPattern string) string { | ||
| // Get the project root dynamically | ||
| _, filename, _, _ := runtime.Caller(0) | ||
| projectRoot := filepath.Dir(filepath.Dir(filepath.Dir(filename))) // Go up 3 levels from e2e/components/ | ||
| componentsPath := filepath.Join(projectRoot, "parts", "common", "components.json") | ||
|
|
||
| jsonBytes, err := os.ReadFile(componentsPath) | ||
| if err != nil { | ||
| return "" | ||
| } | ||
|
|
||
| gpuImages := gjson.GetBytes(jsonBytes, "GPUContainerImages") | ||
| for _, gpuImage := range gpuImages.Array() { | ||
| downloadURL := gpuImage.Get("downloadURL").String() | ||
| if strings.EqualFold(downloadURL, downloadURLPattern) { | ||
| version := gpuImage.Get("gpuVersion.latestVersion").String() | ||
| if version != "" { | ||
| return strings.Replace(downloadURL, "*", version, 1) | ||
| } | ||
| } | ||
| } | ||
| return "" | ||
| } | ||
|
|
||
| // GetE2EContainerImage returns the full container image URL for an e2e test container image | ||
| // by looking up the name and version.latestVersion from components.json E2EContainerImages section. | ||
| // The downloadURL pattern contains a wildcard (*) that gets replaced with the version. | ||
| func GetE2EContainerImage(name string) string { | ||
| // Get the project root dynamically | ||
| _, filename, _, _ := runtime.Caller(0) | ||
| projectRoot := filepath.Dir(filepath.Dir(filepath.Dir(filename))) // Go up 3 levels from e2e/components/ | ||
| componentsPath := filepath.Join(projectRoot, "parts", "common", "components.json") | ||
|
|
||
| jsonBytes, err := os.ReadFile(componentsPath) | ||
| if err != nil { | ||
| return "" | ||
| } | ||
|
|
||
| e2eImages := gjson.GetBytes(jsonBytes, "E2EContainerImages") | ||
| for _, e2eImage := range e2eImages.Array() { | ||
| imageName := e2eImage.Get("name").String() | ||
| if strings.EqualFold(imageName, name) { | ||
| downloadURL := e2eImage.Get("downloadURL").String() | ||
| version := e2eImage.Get("version.latestVersion").String() | ||
| if version != "" { | ||
| return strings.Replace(downloadURL, "*", version, 1) | ||
| } | ||
| } | ||
| } | ||
| return "" | ||
| } |
There was a problem hiding this comment.
GetE2EContainerImage (and GetGPUContainerImage) silently return an empty string if parts/common/components.json can’t be read or the image isn’t found. Callers don’t validate the return value, so failures will surface later as confusing Kubernetes errors (e.g., DaemonSet created with an empty image). Consider returning (string, error) (or accepting a testing.TB to fail fast) and having call sites assert non-empty results.
| // getNvidiaDevicePluginImage returns the full container image URL for the NVIDIA device plugin | ||
| // by reading the version from components.json E2EContainerImages section. | ||
| func getNvidiaDevicePluginImage() string { | ||
| return components.GetE2EContainerImage(nvidiaDevicePluginImageName) |
There was a problem hiding this comment.
getNvidiaDevicePluginImage() directly returns the lookup result without checking for an empty string. If the components.json entry is missing/misparsed, the DaemonSet will be created with an invalid image and the failure will be indirect. Add a fast-fail check (e.g., validate non-empty before constructing the DaemonSet or when deploying it).
| return components.GetE2EContainerImage(nvidiaDevicePluginImageName) | |
| image := components.GetE2EContainerImage(nvidiaDevicePluginImageName) | |
| if strings.TrimSpace(image) == "" { | |
| panic("nvidia device plugin image is not configured in components.json (empty image returned)") | |
| } | |
| return image |
| func Test_Ubuntu2204_GPUNC(t *testing.T) { | ||
| runScenarioUbuntu2204GPU(t, "Standard_NC6s_v3") | ||
| RunScenario(t, &Scenario{ | ||
| Description: "Tests that a GPU-enabled node with Standard_NC6s_v3 can bootstrap and run NVIDIA device plugin as a DaemonSet", | ||
| Tags: Tags{ |
There was a problem hiding this comment.
PR description mentions a new dedicated test Test_Ubuntu2204_NvidiaDevicePlugin_Daemonset, but that test doesn’t exist in the codebase anymore (the DaemonSet validation is now embedded into Test_Ubuntu2204_GPUNC / Test_Ubuntu2204_GPUA10). Update the PR description (or reintroduce a dedicated test) so the change is discoverable and future readers know where this coverage lives.
Summary
Test_Ubuntu2204_NvidiaDevicePlugin_Daemonset) that validates the NVIDIA device plugin works when deployed as a Kubernetes DaemonSet instead of the systemd serviceChanges
New e2e test (
e2e/scenario_gpu_daemonset_test.go):mcr.microsoft.com/oss/v2/nvidia/k8s-device-plugin:v0.18.2Renovate auto-update (
.github/renovate.json):// renovate: datasource=docker depName=...commentsTest plan
Test_Ubuntu2204_NvidiaDevicePlugin_Daemonsete2e test